条件类型
有助于描述输入
和输出
之间的关系。
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
条件类型
看起来和三元表达式很像
:
SomeType extends OtherType ? TrueType : FalseType;
当extends
左边的类型可以分配给右边
的类型,那么表达式会返回左边的true分支类型(TrueType)
;否则将会返回后面的表达式的类型(FalseTypea)
。
条件类型
在泛型
中使用时是非常强大的。
举个例子,让我们看一个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";
}
这些重载描述了一个JavaScript
函数,可以通过传入的参数来选择对应的重载
。我们注意到:
- 如果使用一个库的api必须去一遍又一遍选择对应的
重载
时,这会变得非常麻烦。 - 我们必须去创建三个重载:其中两个是我们确定的类型(一个
string
的输入,一个number
的输入),并且还有一个通用的情况(兼容string | number
)。createLabel
可以处理每一种类型。
换种方式,我们可以用条件类型
来重构:
type NameOrId<T extends number | string> = T extends number
? IdLabel
: NameLabel;
我们可以用条件类型
来简化我们的重载
变为一个不使用重载
的函数。
function createLabel<T extends number | string>(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
条件类型约束(Conditional Type Constraints)
通常来说,条件类型
的检查会提供给我们新的信息。就像用类型守卫``来收缩类型
可以拿到指定的类型一样,条件类型
的true
分支通过我们的类型检查将进一步缩小泛型参数的范围。
举个例子:
type MessageOf<T> = T["message"];
// Type '"message"' cannot be used to index type 'T'.
在这个例子当中,TypeScript
发生了错误,因为T
没有message
属性。我们可以限制T
来消除报错:
type MessageOf<T extends { message: unknown }> = T["message"];
interface Email {
message: string;
}
type EmailMessageContents = MessageOf<Email>;
// type EmailMessageContents = string
然而,如果我们想让MessageOf
可以输入任何类型,并且当输入没有message
属性的类型时,需要返回never
呢?实际上我们可以通过条件类型
来实现:
type MessageOf<T> = T extends { message: unknown } ? T["message"] : never;
interface Email {
message: string;
}
interface Dog {
bark(): void;
}
type EmailMessageContents = MessageOf<Email>;
// type EmailMessageContents = string
type DogMessageContents = MessageOf<Dog>;
// type DogMessageContents = never
在true
分支当中,TypeScript
将确认T
拥有message
属性。
在另一个例子当中,我们也可以写一个Flatten
的类型来获取数组的元素的类型:
type Flatten<T> = T extends any[] ? T[number] : T;
// Extracts out the element type.
type Str = Flatten<string[]>;
// type Str = string
// Leaves the type alone.
type Num = Flatten<number>;
// type Num = number
当Flatten
给了一个数组类型
,Flatten
使用number
类型的索引来取出string[]
的元素类型。否则,Flatten
只会返回你赋予的类型。
在条件类型中推断(Inferring Within Conditional Types)
我们刚刚在类型约束
上使用条件类型
来拿到我们想要的类型
。条件类型
提供了我们一种可以在true
分支中进行类型推断
的关键字infer
。举个例子,我们可以在Flatten
中推断元素类型代替常用的索引访问类型
:
type Flatten<Type> = Type extends Array<infer Item> ? Item : Type;
在这里,我们使用infer
关键字在泛型中引入
泛型变量Item
来代替T
拿到类型。
我们可以用infer
关键字来写出更加有用的类型别名
。举个例子,我们期望拿到函数的返回类型:
type GetReturnType<Type> = Type 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[]
从具有多个调用签名
的类型推断时(就像函数重载
的类型),会从最后的重载签名
来进行推断。
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
条件类型分发(Distributive Conditional Types)
当条件类型
作用于泛型类型
时,赋予的联合类型
会被分配
。举个例子:
type ToArray<Type> = Type extends any ? Type[] : never;
如果我们在toArray
中插入联合类型
,之后每个类型都将应用联合类型
。
type ToArray<Type> = Type extends any ? Type[] : never;
type StrArrOrNumArr = ToArray<string | number>;
// type StrArrOrNumArr = string[] | number[]
StrArrOrNumArr
中发生了以下类型的分配
:
string | number;
并且映射了联合类型
中的每个类型,就像:
ToArray<string> | ToArray<number>;
得到的最终结果是:
string[] | number[];
通常来说,分发
是最理想的结果。当然为了避免这种结果,可以在extends
关键词的两边加上[]
。
type ToArrayNonDist<Type> = [Type] extends [any] ? Type[] : never;
// 'StrArrOrNumArr' is no longer a union.
type StrArrOrNumArr = ToArrayNonDist<string | number>;
// type StrArrOrNumArr = (string | number)[]