作者:东墨
前言:在编写 typescript 应用的时候,有时候我们会希望复用或者构造一些特定结构的类型,这些类型只从 typescript 靠内建类型和 interface、class 比较难以表达,这时候我们就需要用到类型推导, 而讨论类型推导, 则离不开泛型和推断(#infer), 本文我们只讨论泛型
泛型
从形式上看, typescript 中的泛型如同大多数语言(不包括尚未实现的 Go :P)里的泛型:
// one constrain for array-like object
interface ArrayLike<T> {
readonly length: number;
readonly [n: number]: T;
}
上面的 ArrayLike
表达了类数组结构, 它代表的对象的特征是:
- 有只读的
length
字段; - 其它字段必须是数字, 且每个字段对应的值类型必须为 T, 这里的 T 就是泛型标记
array-like 对象是 Javascript 中很古老的对象, 如声明为 function (...) {...}
格式的函数, 其内部的 arguments 变量就是典型的 array-like 对象.
泛型往往可以用在这些场景:
- 保留字 interface
- 保留字 type
- 命名空间(#namespace) 中的 class(实际上此时它是一个 interface)
- 运行时中的 class
- 函数定义(包括 class 的构造函数)
接下来我们依次说明, 在这些场景中, 类型推导如何发挥它的威力.
基于泛型的类型推导
在利用泛型做类型推导时, 切记:
- 泛型只服务于类型的静态分析, 不服务于 Javascript 运行时
- 在考虑类型推导时的逻辑推算, 不应考虑"它在运行时会得到何种类型", 而应考虑"基于类型本身的特性会得到何种类型"
第 2 点可能有点难以理解, 我们先略过, 在下一篇, 我们会明白这句话的含义.
interface: T 与 keyof
全键可选化
在上一篇层提到, 我们可以通过 keyof
提取一个 interface 的所有键名, 当引入泛型后, keyof 还可以做更有趣的事情:
/**
* Make all properties in T optional
*/
type Partial<T> = {
[P in keyof T]?: T[P];
};
Partial
的作用是: 对 T(T 需要是可被当做 interface 的类型), 求其所有的键名, 并依赖键名, 得到一个结构完全相同, 但其所有键都可选 的新 interface.
比如, 对与 Form, UninitForm 可以是它的全键可选项版本:
interface Form {
name: string
age: number
sex: 'male' | 'female' | 'other'
}
type UninitForm = Partial<Form>
则 UninitForm 等价于:
{
name?: string;
age?: number;
sex?: "male" | "female" | "other";
}
全键必需化
相反, 如果你已经有了 UninitForm, 则你可以得靠 Required
到它的全键必需化版本:
/**
* Make all properties in T required
*/
type Required<T> = {
[P in keyof T]-?: T[P];
};
interface UninitForm {
name?: string;
age?: number;
sex?: "male" | "female" | "other";
}
type AllKeyRequriedForm = Required<UninitForm>
全键只读化
结合 readonly, 我们可以把一个 interface 里所有的键转化为只读
/**
* Make all properties in T readonly
*/
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
强大的 extends 三元推导
对于 js 而言, extends
是扩展类的保留字; 而在 typescript 中, 当 extends 出现的如下的场景时, 它意味着类型推导:
/**
* Extract from T those types that are assignable to U
*/
type Extract<T, U> = T extends U ? T : never;
形如 T extends [DEP] ? [RESULT1] : [RESULT2]
的表达式, 是 typescript 中的一种类型推导式, 它的规则是:
若泛型 T 必须满足 [DEP] 的约束(即 T extends [DEP]
为 true
), 则表达式结果为 [RESULT1]; 反之表达式结果为 [RESULT2]:
- 当 [DEP] 是基本类型时, 如果 T 是对应的基本类型, 则
T extends [DEP]
为true
, 反之为false
- 当 [DEP] 是 interface/class 时, 如果 T 必须满足它的约束, 则
T extends [DEP]
为true
, 反之为false
- 当 [DEP] 是 void/never 时, 按基本类型处理
- 当 [DEP] 是 联合类型, 组成 [DEP] 的类型会依次代入 T 进行运算, 最终的结果是这些运算结果的联合类型
- 当 [DEP] 是 any, 则
T extends [DEP]
恒为true
按照这些规则, 我们来分析一下这个 Exclude
.
/**
* Exclude from T those types that are assignable to U
*/
type Exclude<T, U> = T extends U ? never : T;
分析可知:
Exclude
中有两个必要的泛型标记T
和U
(因为它们都未提供默认泛型)- 如果 T 是联合类型, 则我们会得到 T 中除了 U 之外的所有类型
我们来应用一下 Exclude
type NoOne = Exclude<1 | 2 | 3, 1> // NoOne = 2 | 3
另外, 为了说明上面的第 4 点, 我们可以写一个毫无用处的 NotRealExclude
;
type NotRealExclude<T, U> = T extends U ? U : T;
type Orig = NotRealExclude<1 | 2 | 3, 1> // Orig = 1 | 2 | 3
由于 NotRealExclude
在 T 不符合 U 的时候返回 U, 而在 T 符合 U 的时候又返回 T, 最终的结果是: 组成 T 的所有类型又被重新组装了起来.
extends 乱炖
在了解了 extends
的基本用法后, 我们来看更多的例子:
挑选, 排除, 重组对象的键值
/**
* 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];
};
正如我们常用的 pick 函数 可以提取对象中特定的键值对, Pick
也可以提取 T 中特定的键值定义.
interface Person {
name: string
age: number
sex?: "male" | "female" | "other";
}
/**
* equivalent to { name: string }
*/
type SimplePersonInfo = Pick<Person, 'name'>
既然可以 Pick
, 那也可以 Omit
/**
* Construct a type with the properties of T except for those in type K.
*/
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
/**
* equivalent to { name: string, age: number }
*/
type SimplePersonInfo = Omit<Person, 'sex'>
我们知道, 在对象的索引中, in
关键字可以用于从联合类型中提取类型, 作为 interface 的键名, 比如:
type Ks = 'a' | 'b' | 'c'
type KObject = {
[P in Ks]: any
}
则 KObject 可以包含 'a'
, 'b'
, 'c'
三种类型的键名.
结合 extends, 我们可以轻松地从一个 interface 构建具有同样类型的值的字典, 这就是 Record
:
/**
* Construct a type with a set of properties K of type T
*/
type Record<K extends keyof any, T> = {
[P in K]: T;
};
// construct one family with 3 keys: parent, mom, child
type Family = Record<'parent' | 'mom' | 'child', Person>
这等价于
interface Family {
parent: Person,
mom: Person,
child: Person
}
非 Null 化变量等
/**
* Exclude null and undefined from T
*/
type NonNullable<T> = T extends null | undefined ? never : T;
总结
至此,
- 我们知道了如何利用泛型推出新的类型
- 我们知道了如何将泛型与 extends 三元组, 索引 in 表达式 结合, 对 interface 进行拆解、重组
但到目前为止, 我们处理的场景都限与非函数的 interface(class)/type, 对于函数的 interface, 我们能否进行一些特殊处理, 比如, 对一个已有的函数定义, 提取其第 2 个参数的参数类型? 对于下面这个 func, 我们能否提取出 arg2 的类型?
interface func () {
(arg1: string, arg2: {
bar: string
}): void
}
下一篇我们会讨论, 如何使用 infer 关键字达成这一目标.
其它
基于泛型的类型推导, 理论上从 typescript 2.8 开始(实际上更早, 但 typescript 2.8/3.5 是具有里程碑意义的版本, 故以此划分)就可以实现了, 从 typescript 3.5, 官方内置了一些用于推导的的类型(#type)和接口(#interface), 这些是我们用于学习类型推导的良好案例. 本文用到的所有例子, 都来自于 typescript 内置的 lib.es5.d.ts
.
注意 对于泛型, T
往往是用作泛型标记的第一个选择, T
之于泛型, 好比 foo
之于样例代码