封閉集合可能需要編譯時限制、執行時查找物件、線上表示法,或具備已驗證行為的領域物件。由於這些需求並不等價,TypeScript 提供了不同的表示方式。
分離型別需求與執行時識別 #
如下型別:
type Status = "pending" | "running" | "done";只存在於型別檢查期間。JavaScript 無法列舉 Status、測試未知字串是否屬於它,或為它附加中繼資料。執行時程式碼需要對應的值或驗證函式。
相較之下,原生 enum 會輸出 JavaScript 物件並引入列舉型別。as const 表會輸出一般物件,並從中推導型別。類別會輸出建構函式,且能強制執行不變量。應根據程式實際需要的執行時識別進行選擇。
理解原生列舉的輸出 #
數值列舉會指派數字,並輸出正向與反向對映:
enum StatusCode {
Pending,
Running,
Done,
}
console.log(StatusCode.Running); // 1
console.log(StatusCode[1]); // "Running"
產生的物件同時支援從名稱查找數字,以及從數字查找名稱。字串列舉只輸出從名稱到值的屬性:
enum Status {
Pending = "pending",
Running = "running",
Done = "done",
}自 TypeScript 5.0 起,每個列舉成員都有獨立的成員型別,包括從計算值初始化的成員。這可支援型別縮小與窮舉式 switch,但不會改變輸出的物件。
const enum 要求 TypeScript 將成員值內嵌並消除列舉:
const enum Direction {
Left,
Right,
}內嵌可能與 isolated-module 轉譯器衝突,也可能與這類函式庫衝突:其發佈的宣告生命週期長於用來編譯取用者的確切列舉值。除非能控制整個工具鏈與版本策略,否則應避免跨越套件邊界匯出 ambient const enum 宣告。
對僅限型別的集合使用字面值聯集 #
字面值聯集不需要輸出的列舉機制即可符合 JSON 形狀的 API:
type Status = "pending" | "running" | "done";
function label(status: Status): string {
switch (status) {
case "pending":
return "Pending";
case "running":
return "Running";
case "done":
return "Done";
}
}控制流程分析會在每個分支中縮小 status。聯集也能區分攜帶不同資料的變體:
type Payment =
| { kind: "card"; brand: string; lastFour: string }
| { kind: "cash" }
| { kind: "wire"; iban: string };在 payment.kind === "card" 之後,編譯器會公開 brand 和 lastFour。聯集會檢查已經擁有 Payment 的程式碼,但不會在執行時驗證不受信任的 JSON 值。
從 as const 表推導型別
#
值表能將執行時驗證與編譯時聯集繫結至單一定義:
const Status = {
Pending: "pending",
Running: "running",
Done: "done",
} as const satisfies Record<string, string>;
type StatusName = keyof typeof Status;
type Status = (typeof Status)[StatusName];
const statuses: ReadonlySet<string> = new Set(Object.values(Status));
function isStatus(value: unknown): value is Status {
return typeof value === "string" && statuses.has(value);
}as const 會防止屬性值擴大為 string。satisfies 會檢查表的形狀,同時保留這些字面值型別。唯讀集合刻意接受任何字串以進行查找;typeof 檢查與集合成員檢查則提供型別述詞所需的執行時證明。
當順序是首要需求時,使用陣列:
const statuses = ["pending", "running", "done"] as const;
type Status = (typeof statuses)[number];當穩定的符號鍵、中繼資料查找,或與現有成員存取方式的相容性是首要需求時,使用物件。
將已驗證的值建模為物件 #
值物件代表一個領域值,其不變量與行為結合在一起。例如,百分比不只是任意數字:
class Percentage {
private constructor(readonly value: number) {}
static from(value: number): Percentage {
if (!Number.isFinite(value) || value < 0 || value > 100) {
throw new RangeError("percentage must be finite and between 0 and 100");
}
return new Percentage(value);
}
asRatio(): number {
return this.value / 100;
}
equals(other: Percentage): boolean {
return this.value === other.value;
}
toJSON(): number {
return this.value;
}
}私有建構函式會強制呼叫端經過驗證。方法為領域操作命名,而 toJSON 定義邊界表示法。由於 JSON 解析會回傳數字,而不是類別執行個體,因此反序列化必須再次呼叫 Percentage.from。
不要只為了包裝標籤而使用類別。當物件能保護不變量、提供行為,或避免結構相似的基本型別相互混用時,配置與轉換程式碼才具有正當性。
明確設計序列化邊界 #
線上格式應使用穩定且有文件說明的字串或數字,而不是依賴 TypeScript 輸出的物件形狀。數值列舉的自動遞增值容易因成員重新排序而失效;反向對映是實作細節,不是協定。
在輸入邊界執行以下步驟:
- 解析不受信任的表示法。
- 驗證成員關係或建構值物件。
- 在應用程式內保留已驗證的型別。
- 寫入 JSON、資料庫值或訊息時,刻意進行轉換。
當取用者需要列舉輸出的執行時識別,且工具鏈接受該輸出時,使用原生列舉。對於僅限型別的集合,使用字面值聯集;當程式碼也需要執行時值時,使用 as const 表;對於變體特定資料,使用可辨識聯集;對於驗證與行為,使用值物件。
評論
在 GitHub 上閱讀或評論(發表評論需要登入)