深入探討

宣告檔案理論:深度解析

構建模組以提供你所期望的確切 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 的值。

再次明確一下,以下事物會建立值:

  • letconstvar 宣告
  • 包含值的 namespacemodule 宣告
  • 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

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);

這工作得很好,但我們可能想象 SomeTypeSomeVar 關係非常緊密,以至於你希望它們具有相同的名稱。我們可以使用組合將這兩個不同的物件(值和型別)呈現為同一個名稱 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

請注意,我們不能使用介面向類型別名(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(類的例項形態)
  • 作為 X 值屬性的值 Z(類的建構函式)

第二個塊建立了以下名稱含義

  • 作為 X 值屬性的值 Y(型別為 number
  • 名稱空間 Z
  • 作為 X 值屬性的值 Z
  • X.Z 名稱空間中的型別 C
  • 作為 X.Z 值屬性的值 C
  • 型別 X

TypeScript 文件是一個開源專案。請傳送 Pull Request 幫助我們改進這些頁面 ❤

此頁面的貢獻者
MHMohamed Hegazy (54)
OTOrta Therox (12)
1+

最後更新:2026 年 3 月 27 日