大多数实用程序的核心都需要根据输入做出决策。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>;