条件类型有助于描述输入和输出之间的关系。
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)[]