宣告檔案理論:深入探討
建構模組以提供您想要的精確 API 樣貌可能很棘手。例如,我們可能想要一個模組,可以使用或不使用 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
,我們可能會找到多達三個不同的意義:一個類型、一個值或一個命名空間。名稱的解釋方式取決於它被使用的脈絡。例如,在宣告 let m: A.A = A;
中,A
首先被用作一個命名空間,然後作為一個類型名稱,最後作為一個值。這些意義可能會最終指向完全不同的宣告!
這可能看起來很混亂,但只要我們不過度載入東西,它實際上非常方便。讓我們看看這種組合行為的一些有用的方面。
內建組合
精明的讀者會注意到,例如,class
同時出現在類型和值清單中。宣告class C { }
會建立兩件事:一個類型C
,它指的是類別的實例形狀,以及一個值C
,它指的是類別的建構函數。列舉宣告的行為類似。
使用者組合
假設我們寫了一個模組檔foo.d.ts
ts
export var SomeVar: { a: SomeType };export interface SomeType {count: number;}
然後使用它
ts
import * as foo from "./foo";let x: foo.SomeType = foo.SomeVar.a;console.log(x.count);
這運作得很好,但我們可以想像 SomeType
和 SomeVar
非常密切相關,因此您希望它們具有相同的名稱。我們可以使用組合將這兩個不同的物件(值和類型)顯示在同一個名稱 Bar
下
ts
export var Bar: { a: Bar };export interface Bar {count: number;}
這為使用中的程式碼提供了一個非常好的解構機會
ts
import { 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
ts
interface Foo {x: number;}// ... elsewhere ...interface Foo {y: number;}let a: Foo = ...;console.log(a.x + a.y); // OK
這也適用於類別
ts
class Foo {x: number;}// ... elsewhere ...interface Foo {y: number;}let a: Foo = ...;console.log(a.x + a.y); // OK
請注意,我們無法使用 interface
新增到類型別名 (type s = string;
)。
使用 namespace
新增
namespace
宣告可以用來新增新的類型、值和命名空間,只要不產生衝突即可。
例如,我們可以新增一個靜態成員到類別
ts
class C {}// ... elsewhere ...namespace C {export let x: number;}let y = C.x; // OK
請注意,在這個範例中,我們新增一個值到 C
的靜態面 (它的建構函式)。這是因為我們新增一個值,而所有值的容器是另一個值 (類型由命名空間包含,命名空間由其他命名空間包含)。
我們也可以將命名空間類型新增到類別
ts
class C {}// ... elsewhere ...namespace C {export interface D {}}let y: C.D; // OK
在此範例中,在我們為其撰寫 `namespace` 宣告之前,沒有命名空間 `C`。`C` 作為命名空間的意義,不會與類別建立的 `C` 值或類型意義衝突。
最後,我們可以使用 `namespace` 宣告執行許多不同的合併。這不是特別實際的範例,但展示了各種有趣的行為
ts
namespace 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` 中的類型 `Y`
- 命名空間 `X` 中的類型 `Z` (類別的執行個體形狀)
- 值 `Z`,為 `X` 值的屬性 (類別的建構函式)
第二個區塊建立下列名稱意義
- 值 `Y` (類型為 `number`),為 `X` 值的屬性
- 命名空間 `Z`
- 值 `Z`,為 `X` 值的屬性
- 命名空間 `X.Z` 中的類型 `C`
- 值 `C`,為 `X.Z` 值的屬性
- 類型 `X`