Typescript 回調(diào)函數(shù)、事件偵聽(tīng)的類型定義與注釋
實(shí)際項(xiàng)目中會(huì)運(yùn)到的 Typescript 回調(diào)函數(shù)、事件偵聽(tīng)的類型定義,如果剛碰到會(huì)一臉蒙真的,我就是
這是第一次我自己對(duì) Typescript 記錄學(xué)習(xí),所以得先說(shuō)一下我與 Typescript 的孽緣
記得最早是在2014年遇上 Typescript 當(dāng)時(shí)是完全看不上這東西的,甚至帶著鄙視的心態(tài),到不是因?yàn)樗仍?Js 要多寫(xiě)很多代碼而是
作為一名前端老兵遇上 Typescript 的語(yǔ)法與類型就會(huì)讓我想起剛工作時(shí)學(xué)習(xí)的 Flash Actionscript3.0 腳本時(shí)代。不能說(shuō)是完全相同,簡(jiǎn)直是一模一樣。
大約2006年 Adobe 的 flash 9 就開(kāi)發(fā)了自己的新腳本語(yǔ)言 ActionScript 3 完全符合 ECMAScript 第四版規(guī)范, 也就是ES4
與當(dāng)時(shí)代的 Javascript 還處于刀耕火種不同,在 Flash 編輯器中使用 ActionScript3 編寫(xiě)代碼就有了比較完善的類型檢測(cè)與類型提示。
曾經(jīng)的 Actionscript3.0 輝煌的時(shí)代,那時(shí)動(dòng)畫(huà)有Flash, 應(yīng)用有 Flex, 跨平臺(tái)桌面應(yīng)用有 Adobe air ,后面還支持移動(dòng)端,這些用的都是 Actionscript3.0 腳本
而 Actionscript3.0 在我熟練掌握后退出了歷史舞臺(tái)。。。多棒的腳本語(yǔ)言啊,我又白學(xué)了。原因大致是 Adobe 的不上進(jìn)和其它大公司的聯(lián)合圍剿
所以當(dāng)我第一次接觸到 Typescript 的時(shí)候內(nèi)心非常抵觸。
這幾年 Javascript 跟其它語(yǔ)言相比可能還差一大截,但已和當(dāng)年刀耕火種不同,前端工具與框架層出不窮,快速更新迭代 web 應(yīng)用越來(lái)越復(fù)雜,前端工具越來(lái)越成熟,Typescript 的應(yīng)用
也就水到渠成了。當(dāng)在團(tuán)隊(duì)中使用 Typescript 雖然多寫(xiě)了點(diǎn)兒類型代碼,但是好處太多了,可以說(shuō)是用了就回不去了
我們這樣的小角色怎能與時(shí)代洪流相抵呢,隨波逐流吧,學(xué)吧學(xué)到廢為止

如果你學(xué)過(guò) Actionscript3 那么對(duì) Typescript 中普通的,類、接口、繼承、變量類型等概念與語(yǔ)法就會(huì)非常熟悉
唯一沒(méi)有且用的比較廣泛的概念當(dāng)屬 Typescirpt 中的 "泛型" , 泛型的理解與運(yùn)用自我感覺(jué)是比較難的,但又不能不面對(duì),只能多看多學(xué)了
我所學(xué)到與理解的也是看的其它人分享的資料,拾人牙慧
最討厭別人寫(xiě)的文章、書(shū),上來(lái)就是一堆概念和名詞解釋。把你繞的云里霧里
我希望的是從實(shí)際運(yùn)用出發(fā),從問(wèn)題開(kāi)始找解決方案。也就是學(xué)了干啥用,得學(xué)以致用才能更好的理解
以下假設(shè)你已經(jīng)對(duì) Typescript 已經(jīng)有了一定的基礎(chǔ)了解
如果你從未學(xué)過(guò) Typescript 那么請(qǐng)退出先去學(xué)基礎(chǔ)!
一、回調(diào)函數(shù)的類型提示
注冊(cè)自定義事件,傳入的回調(diào)函數(shù),如果事件類型(事件名)對(duì)應(yīng)的回調(diào)函數(shù)內(nèi)回調(diào)參數(shù)不一樣
那么回調(diào)函數(shù)的類型注釋我們無(wú)能為力,只能用 any ,如下 addEvent 函數(shù),用于注冊(cè)事件
eventType 定義為 string 類型
listener 這個(gè)是函數(shù) Function, 但由于事件類型有多種,對(duì)應(yīng)的回調(diào)函數(shù)也有好多種
這就尬住了,暫時(shí)只能用 (...args: any[]) => any 來(lái)作為 listener 的類型
但這樣還是沒(méi)有辦法明確 listener 里邊有多少個(gè)具體的參數(shù)以及類型
// 自定義注冊(cè)事件函數(shù)的類型注釋
const addEvent = (eventType: string, listener:(...args: any[]) => any) => { ? ?console.log(eventType, listener);
? ?
}addEvent('eventTypeName1', () => {
})
如果是這樣,那么 調(diào)用 addEvent 時(shí)回調(diào)函數(shù)是沒(méi)有任何有用的提示的
尬住了是不是
eventType 不同,對(duì)應(yīng)的 listener 也不同
這時(shí)就應(yīng)該想是不是能用泛型來(lái)解決,泛型就是在傳入的時(shí)候才確寫(xiě)具體的類型約束
先建一個(gè)用于映射的類型對(duì)象 MyEventMap, key 是 eventType 類型, value 是對(duì)應(yīng)的 listener 類型
添加泛型 T
用 extends keyof MyEventMap 約束 T 在 MyEventMap 的 key 范圍內(nèi),而key 范圍又是通過(guò) keyof 來(lái)提取的
listener 的類型通過(guò) MyEventMap[T] 來(lái)獲取
type MyEventMap = { ? ?'eventTypeName1': (a: string) => number
? ?'eventTypeName2': (test: boolean) => string[]
}const addEvent = <T extends keyof MyEventMap>(eventType: T, listener: MyEventMap[T]) => { ? ?console.log(eventType, listener);
}addEvent('eventTypeName1', (a) => {
? ?return 1})
這樣就有提示了,看效果

兩個(gè)關(guān)鍵點(diǎn)
extends 來(lái)約束 eventType
MyEventMap[T] 來(lái)獲取具體的 listener 類型
二、代理 DOM 事件的類型注釋
比如你自己在寫(xiě) Js 框架,其中需求是要實(shí)現(xiàn) addEventListener 的代理函數(shù),如何給這個(gè)代理函數(shù)寫(xiě)ts注釋呢?
on('click', ()=> {})
?這樣的方法,且能提示 Typescript 默認(rèn)提供的類型,并約束 eventName 在dom事件
const on = (eventName: string, ?listener: (...args: any[]) => any) => { ? ?console.log(eventName, listener);
}
這樣寫(xiě)也通過(guò)了檢測(cè)...那肯定不行,因?yàn)樾枨笫羌s束為 dom 事件,但現(xiàn)在約束了eventName為 string
on('click', () => {
})
又尬住了,我們得在 ts 提供的 lib.dom.d.ts 文件內(nèi)找答案
源碼中找到?interface HTMLElement
?的接口定義
addEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
顯然 HTMLElementEventMap 就是我們要找的通過(guò) eventName 映射 具體回調(diào)的 Map
那就和上面自定義注冊(cè)函數(shù)一樣處理就可以了即
const on = <T extends keyof HTMLElementEventMap>(eventName: T, ?listener: (this: HTMLElement, ev: HTMLElementEventMap[T]) => any, options?: boolean | AddEventListenerOptions) => { ? ?console.log(eventName, listener, options);
}
這樣就都有正常的提示了
on('mousedown', (e) => { ? ?console.log(e)
})

on('paste', (e) => { ? ?console.log(e)
})

三、fetch 的 ts 注釋
很多情況下,我們會(huì)給 fetch 請(qǐng)求回來(lái)的函數(shù)用?data as User[]
?來(lái)主動(dòng)告訴編譯器返回?cái)?shù)據(jù)的類型, 雖然能用,但不優(yōu)雅

我們會(huì)順著思路,我們?cè)囋嚱o fetch 請(qǐng)求函數(shù)作 ts 注釋
一樣先建一個(gè) ResponseMap ,key 是 三、fetch 請(qǐng)求地址 value 是 fetch 返回的數(shù)據(jù)類型
type ResponseMap = { ? ?'hello/world': number
? ?'test/getlist': string[]
}
const get = async <T extends keyof ResponseMap>(url: T):Promise<ResponseMap[T]> => { ? ?const response = await fetch(url); ? ?return response.json();
}
測(cè)試一下
get('hello/world')

get('test/getlist')

試了一下挺完美,但是,但是,肯定沒(méi)這么簡(jiǎn)單,請(qǐng)求地址很多情況下是帶有參數(shù)的
get('test/getlist?a=1&b=2')
發(fā)現(xiàn)提示錯(cuò)誤,通不過(guò)校驗(yàn)了

果然類型很麻煩。。。

?。。⌒枰倪M(jìn)一下泛型匹配
const get = async <T extends keyof ResponseMap>(url: T ?| `${T}?${string}`):Promise<ResponseMap[T]> => { ? ?const response = await fetch(url); ? ?return response.json();
}get('test/getlist?a=1&b=2')
這下可以通過(guò)校驗(yàn)了,提示也正常工作

關(guān)鍵在于
url: T |?${T}?${string}
這一句的改動(dòng), 通過(guò)字符串模板提取出 T 來(lái)
最后,人家的建議是泛型也需要更友好的命名,T、K、R、等等都太不友好了,可以更具名化如下, 把范圍名字變的更具體
const addEvent = <EventType extends keyof MyEventMap>(eventType: EventType, listener: MyEventMap[EventType]) => { ? ?console.log(eventType, listener);
}const get = async <FetchUrl extends keyof ResponseMap>(url: FetchUrl ?| `${FetchUrl}?${string}`):Promise<ResponseMap[FetchUrl]> => { ? ?const response = await fetch(url); ? ?return response.json();
}
說(shuō)明:以上知識(shí)是看到國(guó)外某個(gè)講 typescript 的視頻中學(xué)到的,沒(méi)找到原視頻