條件類型

在大部分有用的程式中,我們必須根據輸入做出決策。JavaScript 程式也不例外,但由於可以輕易內省值,這些決策也基於輸入的類型。條件類型有助於描述輸入和輸出類型之間的關係。

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 左側的類型可指派給右側的類型時,您將取得第一個分支(「true」分支)中的類型;否則,您將取得後一個分支(「false」分支)中的類型。

從上述範例來看,條件類型可能不會立即顯得有用 - 我們可以告訴自己 Dog extends Animal 是否為 true,並選擇 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

分配式條件型別

當條件類型作用於泛型類型時,給定聯合類型時,它們會變成分配式。例如,採用下列

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+

最後更新:2024 年 3 月 21 日