条件类型 - TypeScript 创建类型
条件类型
很多时候,我们需要基于输入的值来决定输出的值,同样我们也需要基于输入的值的类型来决定输出的值的类型。条件类型(Conditional types)就是用来帮助我们描述输入类型和输出类型之间的关系。
interface Animal { live(): void; } interface Dog extends Animal { woof(): void; } type Example1 = Dog extends Animal ? number : string; // type Example1 = number type Example2 = RegExp extends Animal ? number : string; // type Example2 = string
条件类型的写法有点类似于 JavaScript 中的条件表达式(condition ? trueExpression : falseExpression
):
SomeType extends OtherType ? TrueType : FalseType;
单从这个例子,可能看不出条件类型有什么用,但当搭配泛型使用的时候就很有用了,让我们拿下面的createLabel
函数为例:
interface IdLabel { id: number /* some fields */; } interface NameLabel { name: string /* other fields */; } function createLabel(id: number): IdLabel; function createLabel(name: string): NameLabel; function createLabel(nameOrId: string | number): IdLabel | NameLabel; function createLabel(nameOrId: string | number): IdLabel | NameLabel { throw "unimplemented"; }
这里使用了函数重载,描述了createLabel
是如何基于输入值的类型不同而做出不同的决策,返回不同的类型。注意这样一些事情:
- 如果一个库不得不在一遍又一遍的遍历 API 后做出相同的选择,它会变得非常笨重。
- 我们不得不创建三个重载,一个是为了处理明确知道的类型,我们为每一种类型都写了一个重载(这里一个是
string
,一个是number
),一个则是为了通用情况(接收一个string | number
)。而如果增加一种新的类型,重载的数量将呈指数增加。
其实我们完全可以用把逻辑写在条件类型中:
type NameOrId = T extends number ? IdLabel : NameLabel;
使用这个条件类型,我们可以简化掉函数重载:
function createLabel(idOrName: T): NameOrId { throw "unimplemented"; } let a = createLabel("typescript"); // let a: NameLabel let b = createLabel(2.8); // let b: IdLabel let c = createLabel(Math.random() ? "hello" : 42); // let c: NameLabel | IdLabel
条件类型约束
通常,使用条件类型会为我们提供一些新的信息。正如使用类型保护(type guards)可以收窄类型(narrowing)为我们提供一个更加具体的类型,条件类型的true
分支也会进一步约束泛型,举个例子:
type MessageOf = T["message"]; // Type '"message"' cannot be used to index type 'T'.
TypeScript 报错是因为T
不知道有一个名为message
的属性。我们可以约束T
,这样 TypeScript 就不会再报错:
type MessageOf = T["message"]; interface Email { message: string; } type EmailMessageContents = MessageOf; // type EmailMessageContents = string
但是,如果我们想要MessgeOf
可以传入任何类型,但是当传入的值没有message
属性的时候,则返回默认类型比如never
呢?
我们可以把约束移出来,然后使用一个条件类型:
type MessageOf = T extends { message: unknown } ? T["message"] : never; interface Email { message: string; } interface Dog { bark(): void; } type EmailMessageContents = MessageOf; // type EmailMessageContents = string type DogMessageContents = MessageOf; // type DogMessageContents = never
在true
分支里,TypeScript 会知道T
有一个message
属性。
再举一个例子,我们写一个Flatten
类型,用于获取数组元素的类型,当传入的不是数组,则直接返回传入的类型:
type Flatten = T extends any[] ? T[number] : T; // Extracts out the element type. type Str = Flatten; // type Str = string // Leaves the type alone. type Num = Flatten; // type Num = number
注意这里使用了索引访问类型(opens new window)里的number
索引,用于获取数组元素的类型。
在条件类型里推断
条件类型提供了infer
关键词,可以从正在比较的类型中推断类型,然后在true
分支里引用该推断结果。借助infer
,我们修改下Flatten
的实现,不再借助索引访问类型“手动”的获取出来:
type Flatten = Type extends Array ? Item : Type;
这里我们使用了infer
关键字声明了一个新的类型变量Item
,而不是像之前在true
分支里明确的写出如何获取T
的元素类型,这可以解放我们,让我们不用再苦心思考如何从我们感兴趣的类型结构中挖出需要的类型结构。
我们也可以使用infer
关键字写一些有用的类型帮助别名(helper type aliases)。举个例子,我们可以获取一个函数返回的类型:
type GetReturnType = Type extends (...args: never[]) => infer Return ? Return : never; type Num = GetReturnType number>; // type Num = number type Str = GetReturnType string>; // type Str = string type Bools = GetReturnType boolean[]>; // type Bools = boolean[]
当从多重调用签名(就比如重载函数)中推断类型的时候,会按照最后的签名进行推断,因为一般这个签名是用来处理所有情况的签名。
declare function stringOrNum(x: string): number; declare function stringOrNum(x: number): string; declare function stringOrNum(x: string | number): string | number; type T1 = ReturnType; // type T1 = string | number
分配条件类型
当传入的类型参数为联合类型时,他们会被分配类型。举个例子:
type ToArray = Type extends any ? Type[] : never;
如果我们在ToArray
传入一个联合类型,这个条件类型会被应用到联合类型的每个成员:
type ToArray = Type extends any ? Type[] : never; type StrArrOrNumArr = ToArray; // type StrArrOrNumArr = string[] | number[]
让我们分析下StrArrOrNumArr
里发生了什么,这是我们传入的类型:
string | number;
接下来遍历联合类型里的成员,相当于:
ToArray | ToArray;
所以最后的结果是:
string[] | number[];
通常这是我们期望的行为,如果你要避免这种行为,你可以用方括号包裹extends
关键字的每一部分。
type ToArrayNonDist = [Type] extends [any] ? Type[] : never; // 'StrArrOrNumArr' is no longer a union. type StrArrOrNumArr = ToArrayNonDist; // type StrArrOrNumArr = (string | number)[]
鹏仔微信 15129739599 鹏仔QQ344225443 鹏仔前端 pjxi.com 共享博客 sharedbk.com
图片声明:本站部分配图来自网络。本站只作为美观性配图使用,无任何非法侵犯第三方意图,一切解释权归图片著作权方,本站不承担任何责任。如有恶意碰瓷者,必当奉陪到底严惩不贷!