大多数有效程序的核心是,我们必须依据输入做出一些决定。 JavaScript 程序也是如此,但是由于值可以很容易地被内省,这些决定也是基于输入的类型。 条件类型 有助于描述输入和输出类型之间的关系。
tsTry
interfaceAnimal {live (): void;}interfaceDog extendsAnimal {woof (): void;}typeExample1 =Dog extendsAnimal ? number : string;typeExample2 =RegExp extendsAnimal ? number : string;
条件类型看起来有点像 JavaScript 中的条件表达式(条件 ? true 表达式 : false 表达式
):
tsTry
SomeType extendsOtherType ?TrueType :FalseType ;
当 extends
左边的类型可以赋值给右边的类型时,你将获得第一个分支(“true” 分支)中的类型;否则你将获得后一个分支(“false” 分支)中的类型。
从上面的例子中,条件类型可能不会立即显得很有用 - 我们可以告诉自己是否 Dog extends Animal
并选择 number
或 string
!
但是条件类型的威力来自于将它们与泛型一起使用。
让我们以下面的 createLabel
函数为例:
tsTry
interfaceIdLabel {id : number /* 一些字段 */;}interfaceNameLabel {name : string /* 其它字段 */;}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;}interfaceDog {bark (): void;}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 >;
在 true 分支中,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
关键字从 true 分支中与之进行比较的类型中进行推断的方法。
例如,我们可以在 Flatten
中推断元素类型,而不是使用索引访问类型“手动”提取它:
tsTry
typeFlatten <Type > =Type extendsArray <inferItem > ?Item :Type ;
在这里,我们使用 infer
关键字以声明方式引入一个名为 Item
的新泛型类型变量,而不是指定如何在 true 分支中检索元素类型 T
。
这使我们不必考虑如何挖掘和探索我们感兴趣的类型的结构。
我们可以使用 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>;
这里发生的情况是 StrOrNumArray
分布在以下位置:
tsTry
string | number;
并在联合类型的每个成员类型上映射到有效的内容:
tsTry
ToArray <string> |ToArray <number>;
所以我们得到:
tsTry
string[] | number[];
通常,分布性是所需的行为。
要避免这种行为,可以用方括号括起 extends
关键字的两边。
tsTry
typeToArrayNonDist <Type > = [Type ] extends [any] ?Type [] : never;// 'StrOrNumArr' 不再是一个联合类型typeStrOrNumArr =ToArrayNonDist <string | number>;