在大部分有用的程式中,我們必須根據輸入做出決策。JavaScript 程式也不例外,但由於可以輕易內省值,這些決策也基於輸入的類型。條件類型有助於描述輸入和輸出類型之間的關係。
tsTry
interfaceAnimal {live (): void;}interfaceDog extendsAnimal {woof (): void;}typeExample1 =Dog extendsAnimal ? number : string;typeExample2 =RegExp extendsAnimal ? number : string;
條件類型採用的形式有點類似 JavaScript 中的條件式(condition ? trueExpression : falseExpression
)
tsTry
SomeType extendsOtherType ?TrueType :FalseType ;
當 extends
左側的類型可指派給右側的類型時,您將取得第一個分支(「true」分支)中的類型;否則,您將取得後一個分支(「false」分支)中的類型。
從上述範例來看,條件類型可能不會立即顯得有用 - 我們可以告訴自己 Dog extends Animal
是否為 true,並選擇 number
或 string
!但條件類型的威力來自於將它們與泛型搭配使用。
例如,我們來看看下列 createLabel
函式
tsTry
interfaceIdLabel {id : number /* some fields */;}interfaceNameLabel {name : string /* other fields */;}functioncreateLabel (id : number):IdLabel ;functioncreateLabel (name : string):NameLabel ;functioncreateLabel (nameOrId : string | number):IdLabel |NameLabel ;functioncreateLabel (nameOrId : string | number):IdLabel |NameLabel {throw "unimplemented";}
這些 createLabel
的重載描述了一個單一的 JavaScript 函式,會根據其輸入的類型做出選擇。請注意幾件事
- 如果一個函式庫必須在其整個 API 中不斷做出類似的選擇,這將變得繁瑣。
- 我們必須建立三個重載:一個針對我們確定類型的情況(一個針對
string
,一個針對number
),以及一個針對最通用的情況(採用string | number
)。對於createLabel
可以處理的每一個新類型,重載的數量會呈指數成長。
相反地,我們可以在條件類型中編碼該邏輯
tsTry
typeNameOrId <T extends number | string> =T extends number?IdLabel :NameLabel ;
然後,我們可以使用該條件類型將我們的重載簡化為一個沒有重載的單一函式。
tsTry
functioncreateLabel <T extends number | string>(idOrName :T ):NameOrId <T > {throw "unimplemented";}leta =createLabel ("typescript");letb =createLabel (2.8);letc =createLabel (Math .random () ? "hello" : 42);
條件類型約束
通常,條件型別中的檢查會提供一些新資訊。就像使用類型守衛進行縮小可以提供更具體的型別,條件型別的真分支會進一步透過我們檢查的型別來限制泛型。
例如,我們來看看以下內容
tsTry
typeType '"message"' cannot be used to index type 'T'.2536Type '"message"' cannot be used to index type 'T'.MessageOf <T > =T ["message"];
在此範例中,TypeScript 會出錯,因為不知道 T
有稱為 message
的屬性。我們可以限制 T
,而 TypeScript 將不再抱怨
tsTry
typeMessageOf <T extends {message : unknown }> =T ["message"];interfacemessage : string;}typeEmailMessageContents =MessageOf <
不過,如果我們希望 MessageOf
採用任何型別,而且如果沒有提供 message
屬性,則預設為類似 never
的內容,該怎麼辦?我們可以透過將限制移出並引入條件型別來執行此操作
tsTry
typeMessageOf <T > =T extends {message : unknown } ?T ["message"] : never;interfacemessage : string;}interfaceDog {bark (): void;}typeEmailMessageContents =MessageOf <typeDogMessageContents =MessageOf <Dog >;
在真分支中,TypeScript 知道 T
將 有 message
屬性。
另一個範例是,我們也可以撰寫稱為 Flatten
的型別,將陣列型別扁平化為其元素型別,但否則會保持原狀
tsTry
typeFlatten <T > =T extends any[] ?T [number] :T ;// Extracts out the element type.typeStr =Flatten <string[]>;// Leaves the type alone.typeNum =Flatten <number>;
當 Flatten
獲提供陣列型別時,它會使用索引存取與 number
來擷取 string[]
的元素型別。否則,它只會傳回提供的型別。
條件型別中的推論
我們發現自己使用條件型別來套用約束,然後萃取出型別。這最後成為一個常見的運算,而條件型別讓它變得更容易。
條件型別提供我們一種方式,讓我們可以從我們在真分枝中比較的型別推論出型別,使用 infer
關鍵字。例如,我們可以在 Flatten
中推論出元素型別,而不是使用索引存取型別「手動」擷取它
tsTry
typeFlatten <Type > =Type extendsArray <inferItem > ?Item :Type ;
在這裡,我們使用 infer
關鍵字來宣告性地引入一個新的泛型型別變數,命名為 Item
,而不是在真分枝中指定如何擷取 Type
的元素型別。這讓我們不必思考如何深入挖掘並探查我們有興趣的型別結構。
我們可以使用 infer
關鍵字撰寫一些有用的輔助型別別名。例如,對於簡單的情況,我們可以從函式型別中萃取出回傳型別
tsTry
typeGetReturnType <Type > =Type extends (...args : never[]) => inferReturn ?Return : never;typeNum =GetReturnType <() => number>;typeStr =GetReturnType <(x : string) => string>;typeBools =GetReturnType <(a : boolean,b : boolean) => boolean[]>;
當從具有多個呼叫簽章的型別推論時(例如超載函式的型別),推論會從最後一個簽章進行(推測上,這是最寬容的萬用情況)。無法根據引數型別清單執行超載解析。
tsTry
declare functionstringOrNum (x : string): number;declare functionstringOrNum (x : number): string;declare functionstringOrNum (x : string | number): string | number;typeT1 =ReturnType <typeofstringOrNum >;
分配式條件型別
當條件類型作用於泛型類型時,給定聯合類型時,它們會變成分配式。例如,採用下列
tsTry
typeToArray <Type > =Type extends any ?Type [] : never;
如果我們將聯合類型插入 ToArray
,則條件類型將應用於該聯合的每個成員。
tsTry
typeToArray <Type > =Type extends any ?Type [] : never;typeStrArrOrNumArr =ToArray <string | number>;
這裡發生的是 ToArray
分配在
tsTry
string | number;
並對聯合的每個成員類型進行對應,實際上是
tsTry
ToArray <string> |ToArray <number>;
這讓我們得到
tsTry
string[] | number[];
通常,分配式是期望的行為。若要避免這種行為,你可以用方括號將 extends
關鍵字的每一側包起來。
tsTry
typeToArrayNonDist <Type > = [Type ] extends [any] ?Type [] : never;// 'ArrOfStrOrNum' is no longer a union.typeArrOfStrOrNum =ToArrayNonDist <string | number>;