Typescript 2.8 引入了条件类型,它是类型系统强有力的补充。条件类型允许我们表达更灵活的类型映射,也就是,类型转换会根据不同的条件而不同。
条件类型会先测试一种类型关系,并基于测试结果,选择两种可能类型中的一种。它永远有以下的形式:
T extends U ? X : Y条件类型使用了熟悉的 ... ? ... : ... 语法,它在 Javascript 中被用于条件表达式。T, U, X 和 Y 代表了任意类型。 其中 T extends U 描述了类型关系测试。如果条件满足,类型 X 被选择,否则类型 Y 被选择。
如果使用人类语言,条件类型可以描述如下:如果类型 T 可以赋值给 U,选择类型 X,否则选择类型 Y。
下面的条件类型例子来自于 Typescript lib.es5.d.ts 类型定义文件中预定义的类型:
/**
* Exclude null and undefined from T
*/
type NonNullable<T> = T extends null | undefined ? never : T;如果类型 T 可以赋值给类型 null 或者类型 undefined,NonNullable<T> 类型会选择 never 类型,否则它会使用类型 T。never 类型是 Typescript 的 bottom type,表示这个类型的值不可能存在。
为什么结合条件类型和 never 类型是有用的?因为它允许我们从一个联合类型中移除某些类型。如果条件类型关系测试的是原始的泛型参数(naked type parameter,译者注:也就是 T,而不是 [T] 之类),则条件类型被称为可分配的条件类型(见Distributive conditional types ),它会在联合类型实例化的时候基于分配率进行类型分配。
因为 NonNullable<T> 检查的是原始泛型参数,它可以基于联合类型比如 A | B 根据分配率进行类型分配。这意味着 NonNullable<A | B> 最后等同于 NonNullable<A> | NonNullable<B>。如果,比如 NonNullable<A> 最后运算得到 never 类型,我们可以将 A 从结果的联合类型中去除,有效地过滤 A 这个空类型。同理,相同规则也适用于 NonNullable<B>。
上面的描述相当抽象,让我们来看一个具体的例子。我们定义了一个叫 EmailAddress 的类型别名代表不同类型的联合,其中包括了 null 和 undefined 类型:
type EmailAddress = string | string[] | null | undefined;现在我们将 NonNullable<T> 类型应用于 EmailAddress,然后一步步得到结果类型:
type NonNullableEmailAddress = NonNullable<EmailAddress>;我们先用联合类型替换 EmailAddress 这个类型别名:
type NonNullableEmailAddress = NonNullable<
string | string[] | null | undefined
>;现在就轮到可分配条件类型出场的时候了。我们给一个联合类型应用了 NonNullable<T>,这等同于给联合类型中的所有类型都应用了这个条件类型:
type NonNullableEmailAddress =
| NonNullable<string>
| NonNullable<string[]>
| NonNullable<null>
| NonNullable<undefined>;现在我们用 NonNullable<T> 的定义来替换它:
type NonNullableEmailAddress =
| (string extends null | undefined ? never : string)
| (string[] extends null | undefined ? never : string[])
| (null extends null | undefined ? never : null)
| (undefined extends null | undefined ? never : undefined);接着,我们计算得到每一个条件类型的结果类型。string 和 string[] 都不能赋值给 null | undefined,所以前两个条件类型分别选择了 string 和 string[]。而 null 和 undefined 都可以赋值给 null | undefined,所以后面两种条件类型都选择了 never:
type NonNullableEmailAddress = string | string[] | never | never;因为 never 是任何类型的子类型,所以我们可以将它从联合类型中去除。这样我们就得到了最终的结果类型:
type NonNullableEmailAddress = string | string[];而这就是我们期望得到的类型。
现在让我们来看一个更复杂的例子,它结合了条件类型和映射类型。这里我们定义了一个类型,它可以从一个类型中抽取出所有非空属性值类型对应的 key:
type NonNullablePropertyKeys<T> = {
[P in keyof T]: null extends T[P] ? never : P;
}[keyof T];上面的类型初看可能难以理解。再一次,我尝试通过具体的例子一步一步让它变得更易懂。
假设我们有一个 User 类型,并且想使用 NonNullablePropertyKeys<T> 类型来找出哪些属性是非空的:
type User = {
name: string;
email: string | null;
};
type NonNullableUserPropertyKeys = NonNullablePropertyKeys<User>;下面就讲解如何分析 NonNullablePropertyKeys<User> 类型。首先,我们将 T 类型参数替换为具体的 User 类型:
type NonNullableUserPropertyKeys = {
[P in keyof User]: null extends User[P] ? never : P;
}[keyof User];然后,我们接着解析映射类型中的 keyof User。User 类型包含了两个属性,name 和 email,所以我们可以得到 "name" 和 "email" 字符串字面量组成的联合类型:
type NonNullableUserPropertyKeys = {
[P in "name" | "email"]: null extends User[P] ? never : P;
}[keyof User];接着,我们将 P in ... 映射展开,然后用 "name" 和 "email" 替换 P 类型:
type NonNullableUserPropertyKeys = {
name: null extends User["name"] ? never : "name";
email: null extends User["email"] ? never : "email";
}[keyof User];更进一步,我们可以解析索引访问类型 User["name"] 和 User["email"],得到 User 中 name 和 email 属性的具体类型:
type NonNullableUserPropertyKeys = {
name: null extends string ? never : "name";
email: null extends string | null ? never : "email";
}[keyof User];现在是时候应用我们的条件类型。null 并不能赋值给 string,但它可以赋值给 string | null,我们因此分别得到 "name" 和 never 类型:
type NonNullableUserPropertyKeys = {
name: "name";
email: never;
}[keyof User];现在映射类型和条件类型都解析完了。再一次,我们解析 keyof User:
type NonNullableUserPropertyKeys = {
name: "name";
email: never;
}["name" | "email"];我们现在有一个索引访问类型查询 name 和 email 属性的类型。Typescript 会分别查询这两个类型,然后创建一个查询结果的联合类型:
type NonNullableUserPropertyKeys =
| { name: "name"; email: never }["name"]
| { name: "name"; email: never }["email"];马上就搞定了!我们现在分别查询 name 和 email 两个属性的类型,name 属性有一个 "name" 类型,email 属性有一个 never 类型:
type NonNullableUserPropertyKeys = "name" | never;就如之前所示,我们可以将 never 类型去掉从而得到最终的结果类型:
type NonNullableUserPropertyKeys = "name";完成!User 类型中唯一不可能为空的属性对应的key就是 "name"。
我们把这个例子再往前推一步,定义一个类型来抽取一个给定类型中所有不为空的属性。我们可以使用 Pick<T, K>,它在 lib.es5.d.ts 中被定义:
/**
* From T, pick a set of properties
* whose keys are in the union K
*/
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};我们可以结合 NonNullablePropertyKeys<T> 类型和 Pick<T, K> 类型来定义 NonNullableProperties<T>,它就是我们要得到的类型:
type NonNullableProperties<T> = Pick<T, NonNullablePropertyKeys<T>>;
type NonNullableUserProperties = NonNullableProperties<User>;
// { name: string }如上所示,这就是我们所期望的:在我们的 User 类型中,只有 name 属性是非空的。
另一个条件类型支持的有用特性是通过 infer 关键字来推断类型变量的类型。在一个条件类型的 extends 从句中,你可以使用 infer 关键字来推断一个类型变量,并有效地在类型中运用模式匹配:
type First<T> = T extends [infer U, ...unknown[]] ? U : never;
type SomeTupleType = [string, number, boolean];
type FirstElementType = First<SomeTupleType>; // string注意被推断的类型变量(在这里是 U)只能在条件类型的 true 分支中被使用。
一个被长期呼吁的 Typescript 应该提供的特性是能够从一个给定的函数中抽离出它的返回值类型。下面是 lib.es5.d.ts 文件中定义的 ReturnType<T> 类型的简化版本。它使用了 infer 关键字来推断函数类型的返回值类型:
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;
type A = ReturnType<() => string>; // string
type B = ReturnType<() => () => any[]>; // () => any[]
type C = ReturnType<typeof Math.random>; // number
type D = ReturnType<typeof Array.isArray>; // boolean注意我们需要使用 typeof 来获取 Math.random() 和 Array.isArray() 的返回值类型。我们需要给泛型 T 传递一个类型作为参数,而不是一个值。这是为什么直接写 ReturnType<Math.random> 和 ReturnType<Array.isArray> 是不对的。
如果想了解更多关于 infer 是如何工作的,可以看一下这个pull request,其中 Anders Hejlsberg 介绍了条件类型中的类型推断。
条件类型绝对是 Typescript 类型系统中的高级特性。为了给你更多他们可以被如何使用的例子,我会介绍一些 Typescript lib.es5.d.ts 文件中预定义的条件类型。
我们已经见过和使用过 NonNullable<T> 类型了,它能过过滤出 T 类型中的 null 和 undefined。
它定义如下:
/**
* Exclude null and undefined from T
*/
type NonNullable<T> = T extends null | undefined ? never : T;一些例子:
type A = NonNullable<boolean>; // boolean
type B = NonNullable<number | null>; // number
type C = NonNullable<string | undefined>; // string
type D = NonNullable<null | undefined>; // never注意空类型 D 是如何被运算为 never 的。
Extract<T, U> 类型可以让我们过滤出所有可以赋值给 U 的 T 类型。
它的定义:
/**
* Extract from T those types that are assignable to U
*/
type Extract<T, U> = T extends U ? T : never;一些例子:
type A = Extract<string | string[], any[]>; // string[]
type B = Extract<(() => void) | null, Function>; // () => void
type C = Extract<200 | 400, 200 | 201>; // 200
type D = Extract<number, boolean>; // neverExclude<T, U> 类型可以让我们过滤出所有不能赋值给 U 的 T 类型。它和 Extract<T, U> 是相反的类型。
它的定义:
/**
* Exclude from T those types that are assignable to U
*/
type Exclude<T, U> = T extends U ? never : T;一些例子:
type A = Exclude<string | string[], any[]>; // string
type B = Exclude<(() => void) | null, Function>; // null
type C = Exclude<200 | 400, 200 | 201>; // 400
type D = Exclude<number, boolean>; // number就如上面我们看到的,ReturnType<T> 可以让我们从一个函数类型中抽离出返回值类型。
它的定义:
/**
* Obtain the return type of a function type
*/
type ReturnType<T extends (...args: any[]) => any> = T extends (
...args: any[]
) => infer R
? R
: any;一些例子:
type A = ReturnType<() => string>; // string
type B = ReturnType<() => () => any[]>; // () => any[]
type C = ReturnType<typeof Math.random>; // number
type D = ReturnType<typeof Array.isArray>; // booleanParameters<T> 类型让我们从一个函数类型中抽离出所有的参数类型。它会产生一个包含了所有参数类型的元祖类型(或者是类型 never 如果 T 不是一个函数)。
它的定义:
/**
* Obtain the parameters of a function type in a tuple
*/
type Parameters<T extends (...args: any[]) => any> = T extends (
...args: infer P
) => any
? P
: never;注意 Parameters<T> 和 ReturnType<T> 类型在结构上几乎是一样的。他们最主要的区别是 infer 关键字的位置不同。
一些例子:
type A = Parameters<() => void>; // []
type B = Parameters<typeof Array.isArray>; // [any]
type C = Parameters<typeof parseInt>; // [string, (number | undefined)?]
type D = Parameters<typeof Math.max>; // number[]Array.isArray() 方法期望一个任意类型的参数,这是为什么 B 类型是 [any],一个只包含一个元素的元祖。Math.max() 方法,相反的,期望获得任意多个数字类型的参数(不是单一的一个数组参数),因此,类型 D 被推断为 number[](而不是 [number[]])。
ConstructorParameters<T> 条件类型让我们可以从一个构造函数中抽取所有参数的类型。它也会产生一个包含了所有参数类型的元祖类型(或者是 never 类型如果 T 不是函数的话)。
它的定义:
/**
* Obtain the parameters of a constructor function type in a tuple
*/
type ConstructorParameters<
T extends new (...args: any[]) => any
> = T extends new (...args: infer P) => any ? P : never;注意 ConstructorParameters<T> 类型和 Parameters<T> 几乎是一样的。唯一的不同是添加了 new 关键字来明示这是一个构造函数。
一些例子:
type A = ConstructorParameters<ErrorConstructor>;
// [(string | undefined)?]
type B = ConstructorParameters<FunctionConstructor>;
// string[]
type C = ConstructorParameters<RegExpConstructor>;
// [string, (string | undefined)?]InstanceType<T> 让我们可以从一个构造函数类型中抽取返回值类型。它等同于构造函数的 ReturnType<T> 类型。
它的定义:
/**
* Obtain the return type of a constructor function type
*/
type InstanceType<T extends new (...args: any[]) => any> = T extends new (
...args: any[]
) => infer R
? R
: any;再一次,注意 InstanceType<T> 类型和 ReturnType<T> 以及 ConstructorParameters<T> 在结构上是非常相似的。
一些例子:
type A = InstanceType<ErrorConstructor>; // Error
type B = InstanceType<FunctionConstructor>; // Function
type C = InstanceType<RegExpConstructor>; // RegExp