Typescript 回撥函式、事件偵聽的型別定義與註釋--拾人牙慧

池中物sheldon發表於2023-02-22

實際專案中會運到的 Typescript 回撥函式、事件偵聽的型別定義,如果剛碰到會一臉蒙真的,我就是

這是第一次我自己對 Typescript 記錄學習,所以得先說一下我與 Typescript 的孽緣

記得最早是在2014年遇上 Typescript 當時是完全看不上這東西的,甚至帶著鄙視的心態,到不是因為它比原生 Js 要多寫很多程式碼而是

作為一名前端老兵遇上 Typescript 的語法與型別就會讓我想起剛工作時學習的 Flash Actionscript3.0 指令碼時代。不能說是完全相同,簡直是一模一樣。

大約2006年 Adobe 的 flash 9 就開發了自己的新指令碼語言 ActionScript 3 完全符合 ECMAScript 第四版規範, 也就是ES4

與當時代的 Javascript 還處於刀耕火種不同,在 Flash 編輯器中使用 ActionScript3 編寫程式碼就有了比較完善的型別檢測與型別提示。

曾經的 Actionscript3.0 輝煌的時代,那時動畫有Flash, 應用有 Flex, 跨平臺桌面應用有 Adobe air ,後面還支援移動端,這些用的都是 Actionscript3.0 指令碼

而 Actionscript3.0 在我熟練掌握後退出了歷史舞臺。。。多棒的指令碼語言啊,我又白學了。原因大致是 Adobe 的不上進和其它大公司的聯合圍剿

所以當我第一次接觸到 Typescript 的時候內心非常牴觸。

這幾年 Javascript 跟其它語言相比可能還差一大截,但已和當年刀耕火種不同,前端工具與框架層出不窮,快速更新迭代 web 應用越來越複雜,前端工具越來越成熟,Typescript 的應用

也就水到渠成了。當在團隊中使用 Typescript 雖然多寫了點兒型別程式碼,但是好處太多了,可以說是用了就回不去了

我們這樣的小角色怎能與時代洪流相抵呢,隨波逐流吧,學吧學到廢為止

如果你學過 Actionscript3 那麼對 Typescript 中普通的,類、介面、繼承、變數型別等概念與語法就會非常熟悉

唯一沒有且用的比較廣泛的概念當屬 Typescirpt 中的 "泛型" , 泛型的理解與運用自我感覺是比較難的,但又不能不面對,只能多看多學了

我所學到與理解的也是看的其它人分享的資料,拾人牙慧

最討厭別人寫的文章、書,上來就是一堆概念和名詞解釋。把你繞的雲裡霧裡

我希望的是從實際運用出發,從問題開始找解決方案。也就是學了幹啥用,得學以致用才能更好的理解

以下假設你已經對 Typescript 已經有了一定的基礎瞭解

如果你從未學過 Typescript 那麼請退出先去學基礎!


一、回撥函式的型別提示


註冊自定義事件,傳入的回撥函式,如果事件型別(事件名)對應的回撥函式內回撥引數不一樣

那麼回撥函式的型別註釋我們無能為力,只能用 any ,如下 addEvent 函式,用於註冊事件

eventType 定義為 string 型別

listener 這個是函式 Function, 但由於事件型別有多種,對應的回撥函式也有好多種

這就尬住了,暫時只能用 (...args: any[]) => any 來作為 listener 的型別

但這樣還是沒有辦法明確 listener 裡邊有多少個具體的引數以及型別

// 自定義註冊事件函式的型別註釋

const addEvent = (eventType: string, listener:(...args: any[]) => any) => {
    console.log(eventType, listener);
    
}

addEvent('eventTypeName1', () => { 

})

如果是這樣,那麼 呼叫 addEvent 時回撥函式是沒有任何有用的提示的

尬住了是不是
eventType 不同,對應的 listener 也不同
這時就應該想是不是能用泛型來解決,泛型就是在傳入的時候才確寫具體的型別約束

  1. 先建一個用於對映的型別物件 MyEventMap, key 是 eventType 型別, value 是對應的 listener 型別
  2. 新增泛型 T
  3. 用 extends keyof MyEventMap 約束 T 在 MyEventMap 的 key 範圍內,而key 範圍又是透過 keyof 來提取的
  4. listener 的型別透過 MyEventMap[T] 來獲取
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
})

這樣就有提示了,看效果

兩個關鍵點

  • extends 來約束 eventType
  • MyEventMap[T] 來獲取具體的 listener 型別

二、代理 DOM 事件的型別註釋


比如你自己在寫 Js 框架,其中需求是要實現 addEventListener 的代理函式,如何給這個代理函式寫ts註釋呢?

on('click', ()=> {}) 這樣的方法,且能提示 Typescript 預設提供的型別,並約束 eventName 在dom事件

const on = (eventName: string,  listener: (...args: any[]) => any) => {
    console.log(eventName, listener);
}

這樣寫也透過了檢測...那肯定不行,因為需求是約束為 dom 事件,但現在約束了eventName為 string

on('click', () => {

})

又尬住了,我們得在 ts 提供的 lib.dom.d.ts 檔案內找答案

原始碼中找到 interface HTMLElement 的介面定義

addEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;

顯然 HTMLElementEventMap 就是我們要找的透過 eventName 對映 具體回撥的 Map

那就和上面自定義註冊函式一樣處理就可以了即

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 註釋


很多情況下,我們會給 fetch 請求回來的函式用 data as User[] 來主動告訴編譯器返回資料的型別, 雖然能用,但不優雅

我們會順著思路,我們試試給 fetch 請求函式作 ts 註釋

一樣先建一個 ResponseMap ,key 是 三、fetch 請求地址 value 是 fetch 返回的資料型別

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();
}

測試一下

get('hello/world')

get('test/getlist')

試了一下挺完美,但是,但是,肯定沒這麼簡單,請求地址很多情況下是帶有引數的

get('test/getlist?a=1&b=2')

發現提示錯誤,通不過校驗了

果然型別很麻煩。。。

!!!需要改進一下泛型匹配

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')

這下可以透過校驗了,提示也正常工作

關鍵在於

url: T | ${T}?${string}

這一句的改動, 透過字串模板提取出 T 來


最後,人家的建議是泛型也需要更友好的命名,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();
}

說明:以上知識是看到國外某個講 typescript 的影片中學到的,沒找到原影片內容。當然很多英文內容也沒有翻譯,我只是把理解的知識轉化一下,所以才叫拾人牙慧麼...


cnblogs.com/willian/

https://github.com/willian12345

相關文章