用 TypeScript 定義函式

JavaScript 裡,函式亦物件,我們可以用針對物件的方式來描繪函式的外觀,也可以用 TypeScript 提供的 Namescape 定義。

方法一: 用 Interface 定義

這個方法僅適用於 TypeScript 3.1 以上的版本,舊版請使用 Namespace 定義法

``` enum MouseButton { Left = 1, Middle = 2, Right = 3 } interface ClickEvent { mouseButton: MouseButton } interface EventTarget { (e: any): void autoRemove: boolean } interface ClickEventTarget extends EventTarget { (e: ClickEvent): void } const click: ClickEventTarget = function clickHandler(e: ClickEvent) { if(e.mouseButton === MouseButton.Left) { // do something... } } click.autoRemove = false export default click ```
先分別定義二個 interface EventTarget ClickEventTarget (要只定義一個也行,這裡僅是順便展示 interface 繼承的寫法), ClickEventTarget 繼承自 EventTarget,該 interface 除了描述 function signature ,還要求該 function 必須有 autoRemove 這個屬性,且是布林型態。接著宣告 click 常數,並設定它的型態為 ClickEventTarget, 此時 Vscode 會抱怨常數 click 遺漏了 autoRemove 屬性,所以下一行就可以設定常數 clickautoRemove 屬性,這樣就完成了。


我們也可以將函式宣告與賦值拆開來,像底下這樣的寫法也行:
``` enum MouseButton { Left = 1, Middle = 2, Right = 3 } interface ClickEvent { mouseButton: MouseButton } interface EventTarget { (e: any): void autoRemove: boolean } interface ClickEventTarget extends EventTarget { (e: ClickEvent): void } function clickHandler(e: ClickEvent) { if(e.mouseButton === MouseButton.Left) { // do something... } } clickHandler.autoRemove = false const click: ClickEventTarget = clickHandler export default click ```
不過此種寫法有個缺點: VsCode 不會提示 clickHandler 有 autoRemove 這個屬性,原因也很簡單,因為 clickHandler 跟 interface ClickEventTarget 間沒有任何關聯,請看底下 2 張截圖就明白了:


常數 click 已經明確設定它的 TypeClickEventTarget,所以 VsCode 會列出 ClickEventTarget interface 有的屬性。


如上圖所示,因為 clickHandler 並未被告知其型態是 ClickEventTarget,所以在出現可用屬性提示清單時,看不到 autoRemove。另外也因 clickHandler 未設定 autoRemove 屬性,所以常數 click 也冒出紅蚯蚓。

方法二: Namespace 定義法
``` enum MouseButton { Left = 1, Middle = 2, Right = 3 } interface ClickEvent { mouseButton: MouseButton } export function click(e: ClickEvent) { if(e.mouseButton === MouseButton.Left) { // do something... } } export namespace click { export let autoRemove: boolean = false } ```
跟前面用 interface 方法定義比起來,用 namespace 定義法相較簡潔了許多,這種宣告方式牽涉到以下幾個概念:

Declaration Merging
Namespaces


為避免本篇文章過長而失焦,僅在此列出連結。有興趣的看倌可以自行點進去,上面的連結均是 TypeScript 官網提供。

使用 Namespace 定義法,在 TypeScript compiler(tsc) 生成出對應的 JavaScript 時,會一併產生 click.autoRemove = false 這樣的 Expression。可以說利用 NamespaceDeclaration Merging ,我們可以寫得更輕鬆。

不過使用 Namespace 有個問題,假如你是使用 babel + @babel/preset-typescript 轉換 .ts/.tsx .js 檔的話,因為該 preset 不支援對 TypeScript Namespace 的解析,所以只能用方法一,也就是 interface 的方式來解決了。當然如果是用 tsc 來處理的話,就沒有這個問題了。
.
TypeScriptNamespace 原本的設計是令那些會汙染全域空間的 Javascript Libraries 也能夠定義 Typescript Defintions,這在以前相當常見。隨著 ESM 規格的完善,有許多人認為不應該再繼續使用 Namespace 。ESM 的出現,正好補足了 Javascript 在模組定義、引入與執行上的一片空白 (AMD, UMD & CommonJS 等實作是社群在這方面不斷的演化與嘗試,ESM 則是演化後的結果)。

Namespaces should be deprecated
Ask to support namespace in typescript preset
Namespace & modules
Migrate from global namespaces to modules

至於本文中的 autoRemove 屬性是用來做什麼的? 想像你要設計 on & one method ,前者讓你可以 attach 一個 event handler ,後者可以保證該 event handler 執行完後就會丟棄。所以我們就可以用 autoRemove 達成我們的需求:
``` interface EventHandler { (e: any): void autoRemove: boolean } type AttachedEventHandlers = EventHandler[] interface EventHandlersWithEventName { [name: string]: AttachedEventHandlers } const EventHandlerList: EventHandlersWithEventName = {} export function on(eventName: string, eventHandler: EventHandler) { // bind EventHandler with the eventName... EventHandlerList[eventName] = EventHandlerList[eventName] || [] EventHandlerList[eventName].push(eventHandler) } export function one(eventName: string, EventHandler: EventHandler) { EventHandler.autoRemove = true on(eventName, EventHandler) } export function distpatch(eventName: string, e: any) { if (!Array.isArray(EventHandlerList[eventName])) { return } EventHandlerList[eventName].forEach(EventHandler => EventHandler(e)) EventHandlerList[eventName] = EventHandlerList[eventName].filter(EventHandler => !EventHandler.autoRemove) } ```
one method 裡,指定傳進來的 event handler autoRemove 為真。當 dispatch 函式被呼叫時,執行完對應的 event handler 以後,就移除那些 autoRemove 為真的 event handler ,這些被移除的 event handler 就可以在執行完後被丟棄。不過這個範例不保證這些 event handler 只執行一次,因為它們有可能被重覆加入 EventHandlerList 中,所以有需要的話,可以再加上額外的檢查。

留言