條件型別

大多數實用程式的核心都需要根據輸入做出決策。JavaScript 程式也不例外,但考慮到值很容易被內省(introspected),這些決策也取決於輸入的型別。條件型別(Conditional types)有助於描述輸入型別和輸出型別之間的關係。

ts
interface Animal {
live(): void;
}
interface Dog extends Animal {
woof(): void;
}
 
type Example1 = Dog extends Animal ? number : string;
type Example1 = number
 
type Example2 = RegExp extends Animal ? number : string;
type Example2 = string
Try

條件型別的形式看起來有點像 JavaScript 中的條件表示式(condition ? trueExpression : falseExpression)。

ts
SomeType extends OtherType ? TrueType : FalseType;
Try

extends 左側的型別可賦值給右側的型別時,你將得到第一個分支(“真”分支)的型別;否則,你將得到後者分支(“假”分支)的型別。

從上面的例子來看,條件型別似乎並沒有什麼用處——我們自己就能判斷 Dog extends Animal 是否成立,並手動選擇 numberstring!但條件型別的威力在於將其與泛型結合使用。

例如,我們來看看下面的 createLabel 函式:

ts
interface IdLabel {
id: number /* some fields */;
}
interface NameLabel {
name: string /* other fields */;
}
 
function createLabel(id: number): IdLabel;
function createLabel(name: string): NameLabel;
function createLabel(nameOrId: string | number): IdLabel | NameLabel;
function createLabel(nameOrId: string | number): IdLabel | NameLabel {
throw "unimplemented";
}
Try

這些 createLabel 的過載描述了一個單一的 JavaScript 函式,它根據輸入的型別做出選擇。請注意幾點:

  1. 如果一個庫需要在其整個 API 中反覆做出這種選擇,那麼這種寫法會變得非常繁瑣。
  2. 我們必須建立三個過載:每種我們確定型別的方案一個(一個針對 string,一個針對 number),以及一個針對最通用情況的方案(接收 string | number)。對於 createLabel 能處理的每一種新型別,過載的數量都會呈指數級增長。

相反,我們可以將該邏輯編碼為一個條件型別:

ts
type NameOrId<T extends number | string> = T extends number
? IdLabel
: NameLabel;
Try

然後,我們可以利用該條件型別將過載簡化為一個沒有任何過載的函式。

ts
function createLabel<T extends number | string>(idOrName: T): NameOrId<T> {
throw "unimplemented";
}
 
let a = createLabel("typescript");
let a: NameLabel
 
let b = createLabel(2.8);
let b: IdLabel
 
let c = createLabel(Math.random() ? "hello" : 42);
let c: NameLabel | IdLabel
Try

條件型別約束

通常,條件型別中的檢查會為我們提供一些新資訊。就像使用型別守衛進行收窄可以為我們提供更具體的型別一樣,條件型別的真分支將根據我們檢查的型別進一步約束泛型。

例如,我們來看看下面的例子:

ts
type MessageOf<T> = T["message"];
Type '"message"' cannot be used to index type 'T'.2536Type '"message"' cannot be used to index type 'T'.
Try

在這個例子中,TypeScript 報錯是因為無法確定 T 是否具有名為 message 的屬性。我們可以約束 T,這樣 TypeScript 就不會再報錯了:

ts
type MessageOf<T extends { message: unknown }> = T["message"];
 
interface Email {
message: string;
}
 
type EmailMessageContents = MessageOf<Email>;
type EmailMessageContents = string
Try

但是,如果我們要讓 MessageOf 接收任何型別,並在 message 屬性不可用時預設返回像 never 這樣的型別呢?我們可以透過將約束移出並引入條件型別來實現:

ts
type MessageOf<T> = T extends { message: unknown } ? T["message"] : never;
 
interface Email {
message: string;
}
 
interface Dog {
bark(): void;
}
 
type EmailMessageContents = MessageOf<Email>;
type EmailMessageContents = string
 
type DogMessageContents = MessageOf<Dog>;
type DogMessageContents = never
Try

在真分支內,TypeScript 知道 T 一定擁有一個 message 屬性。

作為另一個例子,我們還可以編寫一個名為 Flatten 的型別,它將陣列型別扁平化為其元素型別,否則保持原樣:

ts
type Flatten<T> = T extends any[] ? T[number] : T;
 
// Extracts out the element type.
type Str = Flatten<string[]>;
type Str = string
 
// Leaves the type alone.
type Num = Flatten<number>;
type Num = number
Try

Flatten 被賦予一個數組型別時,它使用帶有 number 的索引訪問來取出 string[] 的元素型別。否則,它只是返回被賦予的型別。

在條件型別中進行推斷

我們剛剛發現自己正在使用條件型別來應用約束並提取型別。這最終成為了一種非常常見的操作,條件型別使之變得更加容易。

條件型別為我們提供了一種在真分支中使用 infer 關鍵字從我們比較的型別中進行推斷的方法。例如,我們可以在 Flatten 中推斷元素型別,而不是使用索引訪問型別“手動”取出它:

ts
type Flatten<Type> = Type extends Array<infer Item> ? Item : Type;
Try

在這裡,我們使用 infer 關鍵字宣告性地引入了一個名為 Item 的新泛型變數,而不是指定如何在真分支內檢索 Type 的元素型別。這使我們無需思考如何挖掘和剖析我們感興趣的型別結構。

我們可以使用 infer 關鍵字編寫一些有用的輔助類型別名。例如,對於簡單的情況,我們可以從函式型別中提取返回型別:

ts
type GetReturnType<Type> = Type extends (...args: never[]) => infer Return
? Return
: never;
 
type Num = GetReturnType<() => number>;
type Num = number
 
type Str = GetReturnType<(x: string) => string>;
type Str = string
 
type Bools = GetReturnType<(a: boolean, b: boolean) => boolean[]>;
type Bools = boolean[]
Try

當從具有多個呼叫簽名的型別(例如過載函式的型別)進行推斷時,推斷將從最後一個簽名(通常是最具包容性的全部情況)進行。無法基於引數型別列表執行過載解析。

ts
declare function stringOrNum(x: string): number;
declare function stringOrNum(x: number): string;
declare function stringOrNum(x: string | number): string | number;
 
type T1 = ReturnType<typeof stringOrNum>;
type T1 = string | number
Try

分散式條件型別

當條件型別作用於泛型時,當給定聯合型別時,它們就變得分散式(distributive)。例如,請看以下內容:

ts
type ToArray<Type> = Type extends any ? Type[] : never;
Try

如果我們向 ToArray 傳入一個聯合型別,那麼條件型別將應用於該聯合的每個成員。

ts
type ToArray<Type> = Type extends any ? Type[] : never;
 
type StrArrOrNumArr = ToArray<string | number>;
type StrArrOrNumArr = string[] | number[]
Try

這裡發生的事情是 ToArray 分佈在

ts
string | number;
Try

上,並對映聯合型別的每個成員,實際上變成了

ts
ToArray<string> | ToArray<number>;
Try

最終我們得到

ts
string[] | number[];
Try

通常,分散式是期望的行為。要避免這種行為,可以用方括號將 extends 關鍵字的兩側括起來。

ts
type ToArrayNonDist<Type> = [Type] extends [any] ? Type[] : never;
 
// 'ArrOfStrOrNum' is no longer a union.
type ArrOfStrOrNum = ToArrayNonDist<string | number>;
type ArrOfStrOrNum = (string | number)[]
Try

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

此頁面的貢獻者
OTOrta Therox (10)
BKBenedikt König (1)
GFGeorge Flinn (1)
SFShinya Fujino (1)
NMNicolás Montone (1)
9+

最後更新:2026 年 3 月 27 日