深入探討

宣告檔案理論:深入探討

建構模組以提供您想要的精確 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 的值。

同樣地,明確來說,下列事項會建立值:

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

這運作得很好,但我們可以想像 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

請注意,我們無法使用 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`

TypeScript 文件是一個開源專案。透過 傳送 Pull Request 來協助我們改善這些頁面 ❤

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

最後更新:2024 年 3 月 21 日