TypeScript 雖然很方便,但是在引入第三方模組的時候,最怕的就是定義不完整。就某種意義上來說,定義不完整比沒定義還糟:沒定義的話了不起引入的模組整個被當成是 any 也就算了、我們還是可以忘了型別檢查、把相關的部份當成 JavaScript 在寫;然而如果是定義不完整,例如遺漏了一個方法的宣告,那麼每次我們在使用該方法之前還得先把模組轉換成 any 才能去用,這樣一來反而比沒有定義更加麻煩,而且程式碼也很醜。

不過幸好 TypeScript 裡面有模組擴充(module augmentation)的概念可以解決這個問題。

舉例來說,在我最近的一個專案裡面我在寫一些跟 TypeScript AST 有關的程式碼,理所當然地我有引用到 TypeScript 模組本身:

import ts from 'typescript';

可是接下來我就發現,不知為何在其 NPM 套件裡面的 typescript.d.ts 定義當裡面,竟然少了 ts.isPartOfTypeNode() 這個確實有被輸出的方法的定義;我跑回去翻了翻 TypeScript 的原始碼,該方法在 compiler.d.ts 裡面有被宣告,可是這個檔案並沒有跟著 NPM 套件一起被發布。我也搞不懂這到底是故意的還是只是疏忽,但是我是真的需要用到這個方法,而且既然 TypeScript 裡面確實是有,我自己重刻一個也沒道理。假設是疏忽,那我跑去回報 issue 還不知道要等多久才能等到他們釋出新版修正問題,而且如果是故意的(感覺很有可能)那我去回報就根本浪費時間。例如,在 TypeScript 的 GitHub 上頭有一篇 issue 是在請求他們公開 Node.localsSymbol.parent 等等的內部屬性,這則討論已經兩年多了也還是沒有下文。

但是總之在 TypeScript 更新之前,我們都還是可以自己用模組擴充的方式補上這些定義。方法很簡單,只要在自己的專案裡面加上一個 typescript.d.ts 檔案,內容如下:

import 'typescript';

declare module 'typescript' {
    function isPartOfTypeNode(node: Node): boolean;

    interface Node {
        locals?: globalThis.Map<string, Symbol>;
    }

    interface Symbol {
        parent?: Symbol;
    }
}

這樣就可以完全正確使用前述的那些東西了。裡面之所以要寫 globalThis.Map 是因為 TypeScript 模組裡面也有定義一個不同的 Map、直接寫 Map 會打架的緣故。類似地,這裡面提到的 NodeSymbol 都會被解析為 TypeScript 模組底下的同名介面,而不是 DOM 和 ES6 的那兩個原生類別。

分享此頁至:
最後修改日期: 2020/09/11

留言

撰寫回覆或留言

發佈留言必須填寫的電子郵件地址不會公開。