大多數實用程式的核心都需要根據輸入做出決策。JavaScript 程式也不例外,但考慮到值很容易被內省(introspected),這些決策也取決於輸入的型別。條件型別(Conditional types)有助於描述輸入型別和輸出型別之間的關係。
tsTryinterfaceAnimal {live (): void;}interfaceDog extendsAnimal {woof (): void;}typeExample1 =Dog extendsAnimal ? number : string;typeExample2 =RegExp extendsAnimal ? number : string;
條件型別的形式看起來有點像 JavaScript 中的條件表示式(condition ? trueExpression : falseExpression)。
tsTrySomeType extendsOtherType ?TrueType :FalseType ;
當 extends 左側的型別可賦值給右側的型別時,你將得到第一個分支(“真”分支)的型別;否則,你將得到後者分支(“假”分支)的型別。
從上面的例子來看,條件型別似乎並沒有什麼用處——我們自己就能判斷 Dog extends Animal 是否成立,並手動選擇 number 或 string!但條件型別的威力在於將其與泛型結合使用。
例如,我們來看看下面的 createLabel 函式:
tsTryinterfaceIdLabel {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能處理的每一種新型別,過載的數量都會呈指數級增長。
相反,我們可以將該邏輯編碼為一個條件型別:
tsTrytypeNameOrId <T extends number | string> =T extends number?IdLabel :NameLabel ;
然後,我們可以利用該條件型別將過載簡化為一個沒有任何過載的函式。
tsTryfunctioncreateLabel <T extends number | string>(idOrName :T ):NameOrId <T > {throw "unimplemented";}leta =createLabel ("typescript");letb =createLabel (2.8);letc =createLabel (Math .random () ? "hello" : 42);
條件型別約束
通常,條件型別中的檢查會為我們提供一些新資訊。就像使用型別守衛進行收窄可以為我們提供更具體的型別一樣,條件型別的真分支將根據我們檢查的型別進一步約束泛型。
例如,我們來看看下面的例子:
tsTrytypeType '"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 就不會再報錯了:
tsTrytypeMessageOf <T extends {message : unknown }> =T ["message"];interfacemessage : string;}typeEmailMessageContents =MessageOf <
但是,如果我們要讓 MessageOf 接收任何型別,並在 message 屬性不可用時預設返回像 never 這樣的型別呢?我們可以透過將約束移出並引入條件型別來實現:
tsTrytypeMessageOf <T > =T extends {message : unknown } ?T ["message"] : never;interfacemessage : string;}interfaceDog {bark (): void;}typeEmailMessageContents =MessageOf <typeDogMessageContents =MessageOf <Dog >;
在真分支內,TypeScript 知道 T 一定擁有一個 message 屬性。
作為另一個例子,我們還可以編寫一個名為 Flatten 的型別,它將陣列型別扁平化為其元素型別,否則保持原樣:
tsTrytypeFlatten <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 中推斷元素型別,而不是使用索引訪問型別“手動”取出它:
tsTrytypeFlatten <Type > =Type extendsArray <inferItem > ?Item :Type ;
在這裡,我們使用 infer 關鍵字宣告性地引入了一個名為 Item 的新泛型變數,而不是指定如何在真分支內檢索 Type 的元素型別。這使我們無需思考如何挖掘和剖析我們感興趣的型別結構。
我們可以使用 infer 關鍵字編寫一些有用的輔助類型別名。例如,對於簡單的情況,我們可以從函式型別中提取返回型別:
tsTrytypeGetReturnType <Type > =Type extends (...args : never[]) => inferReturn ?Return : never;typeNum =GetReturnType <() => number>;typeStr =GetReturnType <(x : string) => string>;typeBools =GetReturnType <(a : boolean,b : boolean) => boolean[]>;
當從具有多個呼叫簽名的型別(例如過載函式的型別)進行推斷時,推斷將從最後一個簽名(通常是最具包容性的全部情況)進行。無法基於引數型別列表執行過載解析。
tsTrydeclare functionstringOrNum (x : string): number;declare functionstringOrNum (x : number): string;declare functionstringOrNum (x : string | number): string | number;typeT1 =ReturnType <typeofstringOrNum >;
分散式條件型別
當條件型別作用於泛型時,當給定聯合型別時,它們就變得分散式(distributive)。例如,請看以下內容:
tsTrytypeToArray <Type > =Type extends any ?Type [] : never;
如果我們向 ToArray 傳入一個聯合型別,那麼條件型別將應用於該聯合的每個成員。
tsTrytypeToArray <Type > =Type extends any ?Type [] : never;typeStrArrOrNumArr =ToArray <string | number>;
這裡發生的事情是 ToArray 分佈在
tsTrystring | number;
上,並對映聯合型別的每個成員,實際上變成了
tsTryToArray <string> |ToArray <number>;
最終我們得到
tsTrystring[] | number[];
通常,分散式是期望的行為。要避免這種行為,可以用方括號將 extends 關鍵字的兩側括起來。
tsTrytypeToArrayNonDist <Type > = [Type ] extends [any] ?Type [] : never;// 'ArrOfStrOrNum' is no longer a union.typeArrOfStrOrNum =ToArrayNonDist <string | number>;