大多数有用程序的核心都需要根据输入做出决策。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
是否成立,并选择 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);
条件类型约束
通常,条件类型中的检查将为我们提供一些新信息。就像使用类型守卫进行缩小可以为我们提供更具体的类型一样,条件类型的 true 分支将通过我们检查的类型进一步约束泛型。
例如,让我们来看以下例子
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[]>;
当从具有多个调用签名的类型(例如重载函数的类型)推断时,推断将从最后一个签名(可能是最宽松的 catch-all 案例)进行。无法根据参数类型列表执行重载解析。
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>;