TypeScript 条件类型
条件类型
很多时候,我们需要基于输入的值来决定输出的值,同样的我们也需要基于输入的类型来决定输出的类型。条件类型就是用来帮助我们描述输入类型和输出类型之间的关系。
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
,另一个则是为了处理这两种数据类型都有可能出现的情况,如果我们需要新增一个类型,那么重载的数量将呈指数增加。
其实我们完全可以把逻辑写在条件类型中:
type NameOrId<T extends string | number> = T extends string ? NameLabel : IdLabel;
function createLabel<T extends string | number>(idOrName: T): NameOrId<T> {
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
条件类型约束
通常使用条件类型可以为我们提供一些新信息。正如使用类型保护可以收窄类型为我们提供一个更加具体的类型,条件类型的true
分支也会进一步约束类型,举个例子:
type MessageOf<T> = T['message']; // error: 类型“"message"”无法用于索引类型“T”。
这里报错是因为TypeScript并不知道泛型T
有个message
的属性,我们可以约束T
,这样就不会报错:
type MessageOf<T extends { message: unknown }> = T['message'];
interface A {
message: string;
}
type B = MessageOf<A>; // type B = string
这样一来我们就约束了传入的类型必须有message
这个属性,但如果我们想可以传入其他类型,并且如果没有message
属性的时候返回的类型是never
,我们就可以把约束的逻辑抽出来写在条件类型中。
type MessageOf<T> = T extends { message: unknown } ? T['message'] : never;
interface A {
message: string;
}
interface B {
bark(): void
}
type C = MessageOf<A>; // type C = string
type D = MessageOf<B>; // type D = never
在true
分支里,TypeScript会知道T
有message
属性。
再举个例子,我们写一个Flatten
类型,如果传入的类型是数组,就返回数组元素的类型,反之则返回传入的类型:
type Flatten<T> = T extends any[] ? T[number] : T;
type Str = Flatten<string[]>; // type Str = string
type Num = Flatten<number>; // type Num = number
条件类型推断
条件类型提供了infer关键字,可以从正在比较的类型中推断类型,然后在true分支中引用推断出来的结果。借助infer,我们修改下Flatten的实现,不再借助索引访问类型"手动"的获取出来:
type Flatten<T> = T extends Array<infer Item> ? Item : T;
这里我们使用了 infer
关键字声明了一个新的类型变量 Item
,而不是像之前在 true
分支里明确的写出如何获取 T 的元素类型,这可以解放我们,让我们不用再苦心思考如何从我们感兴趣的类型结构中挖出需要的类型结构。
我们也可以使用 infer 关键字写一些有用的 类型帮助别名。举个例子,我们可以获取一个函数返回的类型:
type GetReturnType<T> = T extends (...args: never[]) => infer Return ? Return : never;
type Num = GetReturnType<() => number>; // type Num = number
type Str = GetReturnType<(x: string) => string>; // type Str = string
type Bools = GetReturnType<(a: boolean, b: boolean) => boolean[]>; // type Bools = boolean[]
type Never = GetReturnType<(y: never) => never>; // type Never = never
当从多重调用签名(比如重载函数)中推断类型的时候,会按照最后的签名进行推断,因为这个签名一般是用来处理所有情况的签名:
declare function stringOrNum(x: string): number;
declare function stringOrNum(x: number): string;
declare function stringOrNum(x: string | number): string | number;
type T1 = ReturnType<typeof stringOrNum>;
// type T1 = string | number
条件类型分发
当在泛型中使用条件类型的时候,如果传入的是一个联合类型,就会变成分发的,举个例子:
type ToArray<T> = T extends any ? T[] : never;
type StrArrOrNumArr = ToArray<string | number>; // type StrArrOrNumArr = string[] | number[]
让我们推测一下StrArrOrNumArr
发生了什么,这是我们传入的类型:
string | number
传入ToArray
后进行遍历:
ToArray<string> | ToArray<number>
最终的结果:
string[] | number[]
这通常是我们期望的结果,如果你想避免这种行为,可以使用[]
包裹extends
关键字的每一部分:
type ToArray<T> = [T] extends [any] ? T[] : never;
type StrArrOrNumArr = ToArray<string | number>; // type StrArrOrNumArr = (string | number)[]