用 TypeScript 實(shí)現(xiàn)類(lèi)型安全的 EventEmitter,這下不用怕寫(xiě)錯(cuò)事件名了
大家好,我是前端西瓜哥。最近個(gè)人項(xiàng)目用 EventEmitter 模塊越來(lái)越多了,因?yàn)轭?lèi)型不夠安全,寫(xiě)起來(lái)要很小心。所以打算改良一下,實(shí)現(xiàn) TypeScript 類(lèi)型安全的 EventEmitter,解決事件名和函數(shù)類(lèi)型不能做檢驗(yàn)的問(wèn)題。
Nodejs 的 EventEmitter 是一個(gè)發(fā)布訂閱模塊。
利用該類(lèi),我們可以實(shí)現(xiàn)事件的監(jiān)聽(tīng),被監(jiān)聽(tīng)對(duì)象會(huì)在合適的時(shí)機(jī)觸發(fā)事件,調(diào)用監(jiān)聽(tīng)對(duì)象提供的方法,是模塊間解耦的常用實(shí)現(xiàn)。
配合越來(lái)越流行的 TypeScript,我們可以通過(guò)安裝 @types/node,我們能夠進(jìn)一步獲得類(lèi)型能力,減少低級(jí)錯(cuò)誤的出現(xiàn)。但 EventEmitter 的類(lèi)型實(shí)現(xiàn)并不出色,稱(chēng)不上是類(lèi)型安全。
通常來(lái)說(shuō),不同事件對(duì)應(yīng)的響應(yīng)函數(shù)類(lèi)型是不同的,但 @types/node 的 EventEmiiter 類(lèi)型沒(méi)有提供高級(jí)類(lèi)型,而是給一個(gè)異常寬松的類(lèi)型。
可以看到,on 方法傳入的事件名類(lèi)型是 ??string | symbol?
??,listener 則是隨意任何類(lèi)型的一個(gè)函數(shù)即可。emit 傳入的參數(shù)也是 ??any[]?
?。
因?yàn)檫^(guò)于寬松的類(lèi)型,如果事件名拼錯(cuò)了,TypeScript 并不會(huì)報(bào)錯(cuò),當(dāng)一個(gè) eventEmitter 的事件類(lèi)型變得非常多,我們就和裸寫(xiě) JavaScript 沒(méi)什么區(qū)別了。
自己動(dòng)手,豐衣足食,我們不妨 自己實(shí)現(xiàn)一個(gè)類(lèi)型安全的 EventEmitter。
EventEmitter 實(shí)現(xiàn)
因?yàn)槲移鋵?shí)是在前端用的 EventEmitter,所以寫(xiě)了一個(gè) EventEmitter 簡(jiǎn)易 JavaScript 實(shí)現(xiàn)。
如果你是 nodejs,繼承 EventEmitter 然后改它的類(lèi)型或許是更好的做法,或者可以 “基于組合而不是繼承” 的方式實(shí)現(xiàn)一個(gè)。
類(lèi)型安全的 EventEmitter
接著是將上面的代碼改為 TypeScript。
我們希望的效果是:
EventEmitter 支持接受一個(gè)對(duì)象結(jié)構(gòu)的 interface 作為類(lèi)型參數(shù),指定不同的 key 對(duì)應(yīng)的函數(shù)類(lèi)型。
然后我們?cè)僬{(diào)用 on、emit、off 時(shí),如果事件名、函數(shù)參數(shù)不匹配,編譯就不能通過(guò)。
代碼實(shí)現(xiàn):
讀者朋友可自行拷貝上面兩段代碼到 TypeScript Playground 測(cè)試一下。
簡(jiǎn)單講解一下。
首先是開(kāi)頭的類(lèi)型參數(shù)。
這里的 extends 作用是限定類(lèi)型范圍,防止提供一個(gè)不符合規(guī)則的類(lèi)型參數(shù)。
Record 是 TypeScript 自帶的高級(jí)類(lèi)型,根據(jù)傳入的 key 和 value 創(chuàng)建一個(gè)對(duì)象結(jié)構(gòu)(后面說(shuō)到的 T 就是它)。
value 本來(lái)的類(lèi)型應(yīng)該是 (...args: any[]) => void,好限制為函數(shù)。但在不是非字面量類(lèi)型直傳的情況下無(wú)法通過(guò)類(lèi)型檢測(cè),只好改成 any 了。(坑爹的 Index signature for type 'string' is missing 報(bào)錯(cuò))。
然后是 eventMap,它的實(shí)際內(nèi)容是這樣的:
所以 key 需要為傳入對(duì)象類(lèi)型參數(shù)的 key。
函數(shù)則不用指定特定類(lèi)型,因?yàn)樗撬接械模瑹o(wú)法被類(lèi)外部訪(fǎng)問(wèn),沒(méi)有做過(guò)多的類(lèi)型推斷,就寬松一些,設(shè)置為任何函數(shù)類(lèi)型。
這里我用了對(duì)象字面量,讀者朋友也可以考慮用 Map 數(shù)據(jù)結(jié)構(gòu)。
然后是 on 方法,首先 eventName 必須為 T 的 key 的其中之一,因?yàn)橐茢?K 這么個(gè)內(nèi)部類(lèi)型變量,所以我們要在 on 后面加上 <K extends keyof T>,listener 就是對(duì)應(yīng)的 T[K]。
off 方法同理,不展開(kāi)講。
然后是 emit,第一個(gè) eventName 用 keyof T 沒(méi)問(wèn)題,后面需要取出 handler 的參數(shù),作為剩余參數(shù)。
這里用了 TS 自帶的 Parameters 高級(jí)類(lèi)型,作用是取出函數(shù)的參數(shù)返回一個(gè)數(shù)組類(lèi)型。
臨時(shí)擴(kuò)展自定義事件
如果要給一個(gè)已經(jīng)固定了類(lèi)型的實(shí)例,臨時(shí)加一個(gè)事件,可以用 & 交叉類(lèi)型擴(kuò)展一下。
結(jié)尾
一番改造,我們充分利用 TypeScript 的強(qiáng)大類(lèi)型體操能力,構(gòu)建了一個(gè)類(lèi)型安全的 EventEmitter。寫(xiě)錯(cuò)事件名,函數(shù)類(lèi)型沒(méi)對(duì)上什么的,根本不在怕的。
這次的類(lèi)型體操還算是比較簡(jiǎn)單的。如果再?gòu)?fù)雜一點(diǎn),可讀性就很差了。
TypeScript 的類(lèi)型編程的語(yǔ)法真的很不美觀,可讀性差。如果你不是庫(kù)作者,個(gè)人不建議過(guò)度使用類(lèi)型體操,它像正則一樣,很強(qiáng)大,但也很復(fù)雜。