宣告檔案理論:深度解析
構建模組以提供你所期望的確切 API 形態可能很棘手。例如,我們可能需要一個既可以配合 new 呼叫又可以不配合 new 呼叫從而產生不同型別的模組,該模組還要暴露層級分明的多種命名型別,並且在模組物件上具有一些屬性。
閱讀本指南後,你將掌握編寫複雜宣告檔案的工具,從而能夠暴露友好的 API 介面。本指南重點介紹模組(或 UMD)庫,因為其中的選擇更加多樣化。
關鍵概念
透過理解 TypeScript 如何工作的一些關鍵概念,你可以完全理解如何建立任何形態的宣告。
型別
如果你正在閱讀本指南,可能已經大致瞭解 TypeScript 中的型別是什麼。不過,為了更明確一點,型別是透過以下方式引入的:
- 類型別名宣告 (
type sn = number | string;) - 介面宣告 (
interface I { x: number[]; }) - 類宣告 (
class C { }) - 列舉宣告 (
enum E { A, B, C }) - 引用型別的
import宣告
上述每種宣告形式都建立了一個新的型別名稱。
值
和型別一樣,你可能已經瞭解什麼是值。值是我們在表示式中可以引用的執行時名稱。例如,let x = 5; 建立了一個名為 x 的值。
再次明確一下,以下事物會建立值:
let、const和var宣告- 包含值的
namespace或module宣告 enum宣告class宣告- 引用值的
import宣告 function宣告
名稱空間
型別可以存在於名稱空間中。例如,如果我們有宣告 let x: A.B.C,我們稱型別 C 來自 A.B 名稱空間。
這種區分細微且重要——在這裡,A.B 不一定是一個型別或一個值。
簡單組合:一個名稱,多種含義
給定一個名稱 A,我們可能會發現 A 多達三種不同的含義:型別、值或名稱空間。名稱如何被解釋取決於它所處的上下文。例如,在宣告 let m: A.A = A; 中,A 首先被用作名稱空間,然後用作型別名稱,最後用作值。這些含義可能最終指向完全不同的宣告!
這看起來可能很令人困惑,但只要我們不過度過載事物,它其實非常方便。讓我們看看這種組合行為的一些有用方面。
內建組合
精明的讀者會注意到,例如 class 同時出現在型別和值列表中。宣告 class C { } 建立了兩個東西:一個是型別 C,指的是類的例項形態;另一個是值 C,指的是類的建構函式。列舉宣告的行為類似。
使用者組合
假設我們編寫了一個模組檔案 foo.d.ts
tsexport var SomeVar: { a: SomeType };export interface SomeType {count: number;}
然後使用它
tsimport * as foo from "./foo";let x: foo.SomeType = foo.SomeVar.a;console.log(x.count);
這工作得很好,但我們可能想象 SomeType 和 SomeVar 關係非常緊密,以至於你希望它們具有相同的名稱。我們可以使用組合將這兩個不同的物件(值和型別)呈現為同一個名稱 Bar
tsexport var Bar: { a: Bar };export interface Bar {count: number;}
這為使用程式碼中的解構提供了非常好的機會
tsimport { Bar } from "./foo";let x: Bar = Bar.a;console.log(x.count);
同樣,我們在這裡將 Bar 同時用作型別和值。請注意,我們不必宣告 Bar 值具有 Bar 型別——它們是獨立的。
高階組合
某些型別的宣告可以在多個宣告中合併。例如,class C { } 和 interface C { } 可以共存,並且都為 C 型別貢獻屬性。
只要不產生衝突,這是合法的。一個經驗法則是:值總是與同名的其他值衝突,除非它們被宣告為 namespace;型別如果透過類型別名宣告(type s = string)則會衝突;名稱空間永遠不會衝突。
讓我們看看這是如何使用的。
使用 interface 新增
我們可以使用另一個 interface 宣告向 interface 新增額外的成員
tsinterface Foo {x: number;}// ... elsewhere ...interface Foo {y: number;}let a: Foo = ...;console.log(a.x + a.y); // OK
這也適用於類
tsclass Foo {x: number;}// ... elsewhere ...interface Foo {y: number;}let a: Foo = ...;console.log(a.x + a.y); // OK
請注意,我們不能使用介面向類型別名(type s = string;)新增內容。
使用 namespace 新增
namespace 宣告可以用來以任何不產生衝突的方式新增新的型別、值和名稱空間。
例如,我們可以向類新增靜態成員
tsclass C {}// ... elsewhere ...namespace C {export let x: number;}let y = C.x; // OK
請注意,在此示例中,我們向 C 的靜態側(其建構函式)添加了一個值。這是因為我們添加了一個值,而所有值的容器是另一個值(型別由名稱空間容納,名稱空間由其他名稱空間容納)。
我們還可以向類新增帶名稱空間的型別
tsclass C {}// ... elsewhere ...namespace C {export interface D {}}let y: C.D; // OK
在此示例中,在我們為其編寫 namespace 宣告之前,並沒有名稱空間 C。作為名稱空間的 C 的含義與類建立的 C 的值或型別含義不衝突。
最後,我們可以使用 namespace 宣告執行許多不同的合併。這並不是一個特別真實的示例,但展示了各種有趣的組合行為
tsnamespace X {export interface Y {}export class Z {}}// ... elsewhere ...namespace X {export var Y: number;export namespace Z {export class C {}}}type X = string;
在此示例中,第一個塊建立了以下名稱含義
- 值
X(因為namespace宣告包含一個值Z) - 名稱空間
X(因為namespace宣告包含一個型別Y) X名稱空間中的型別YX名稱空間中的型別Z(類的例項形態)- 作為
X值屬性的值Z(類的建構函式)
第二個塊建立了以下名稱含義
- 作為
X值屬性的值Y(型別為number) - 名稱空間
Z - 作為
X值屬性的值Z X.Z名稱空間中的型別C- 作為
X.Z值屬性的值C - 型別
X