用 TypeScript 定義函式
JavaScript 裡,函式亦物件,我們可以用針對物件的方式來描繪函式的外觀,也可以用 TypeScript 提供的 Namescape 定義。
``` 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 定義
這個方法僅適用於 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 屬性,所以下一行就可以設定常數 click 的 autoRemove 屬性,這樣就完成了。
我們也可以將函式宣告與賦值拆開來,像底下這樣的寫法也行:
```
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 已經明確設定它的 Type 為 ClickEventTarget,所以 VsCode 會列出 ClickEventTarget interface 有的屬性。
如上圖所示,因為 clickHandler 並未被告知其型態是 ClickEventTarget,所以在出現可用屬性提示清單時,看不到 autoRemove。另外也因 clickHandler 未設定 autoRemove 屬性,所以常數 click 也冒出紅蚯蚓。
方法二: Namespace 定義法
不過此種寫法有個缺點: VsCode 不會提示 clickHandler 有 autoRemove 這個屬性,原因也很簡單,因為 clickHandler 跟 interface ClickEventTarget 間沒有任何關聯,請看底下 2 張截圖就明白了:
常數 click 已經明確設定它的 Type 為 ClickEventTarget,所以 VsCode 會列出 ClickEventTarget interface 有的屬性。
方法二: 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。可以說利用 Namespace 與 Declaration Merging ,我們可以寫得更輕鬆。
不過使用 Namespace 有個問題,假如你是使用 babel + @babel/preset-typescript 轉換 .ts/.tsx 成 .js 檔的話,因為該 preset 不支援對 TypeScript Namespace 的解析,所以只能用方法一,也就是 interface 的方式來解決了。當然如果是用 tsc 來處理的話,就沒有這個問題了。
.
TypeScript 的 Namespace 原本的設計是令那些會汙染全域空間的 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 方法定義比起來,用 namespace 定義法相較簡潔了許多,這種宣告方式牽涉到以下幾個概念:
Declaration Merging
Namespaces
為避免本篇文章過長而失焦,僅在此列出連結。有興趣的看倌可以自行點進去,上面的連結均是 TypeScript 官網提供。
使用 Namespace 定義法,在 TypeScript compiler(tsc) 生成出對應的 JavaScript 時,會一併產生 click.autoRemove = false 這樣的 Expression。可以說利用 Namespace 與 Declaration Merging ,我們可以寫得更輕鬆。
不過使用 Namespace 有個問題,假如你是使用 babel + @babel/preset-typescript 轉換 .ts/.tsx 成 .js 檔的話,因為該 preset 不支援對 TypeScript Namespace 的解析,所以只能用方法一,也就是 interface 的方式來解決了。當然如果是用 tsc 來處理的話,就沒有這個問題了。
.
TypeScript 的 Namespace 原本的設計是令那些會汙染全域空間的 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 中,所以有需要的話,可以再加上額外的檢查。


留言
張貼留言