探秘Typescript·Infer与强大的类型计算体系
常见的类型计算场景
联合类型 |
我们之前学习过联合类型,使用|
符号可以将多个独立的类型联合成一个新的类型,如:
typescript
type Shape = Circle | Square | Rect;
// 如上述的 Shape 类型就是将 `Circle`、`Square`和`Rect`三种形状的的联合
交叉类型 &
除了联合类型外,我们还可以用交叉类型将多个类型合并成一个新的类型,如:
typescript
type Point2d = {x: number, y: number};
type ZIndex = {z: number};
type Point3d = Point2d & ZIndex;
const point: Point3d = {
x: 1,
y: 2,
z: 3
};
console.log(point);
// 上面将 Point2d 与 ZIndex 进行了交叉类型计算,最终得到的 Point3d 类型同时拥有了 Point2d 和 ZIndex 类型的所有属性
更复杂的类型操作 Infer
我们先通过一个例子来初识一下Infer
的强大。
现在,我们要实现一个flattern
函数,这个函数可以将传入的任意层级的数组拍平成一维数组,如:
typescript
flattern([[[[[[[], 1, 2]]]]]]);// 最终返回为:[1, 2]
那么这个函数我们要如何进行类型定义呢?或许大家觉得可以这样:
typescript
function flattern(arr: Array<any>): Array<any> {
}
上述的类型描述虽然不会报错,但这样就是去了对数组内元素类型的校验了。
那么,我们根据类型计算再来重新设计一下:
typescript
type Flatterned<T> = T extends Array<V> ? V : T;
type Result = Flatterned<number[]>
// 先不考虑上面的类型定义的合法性,至少从语义上来说,我们拍平的结果类型就是 number 类型的,这样我们就可以把数组中的数组项的类型给抽离出来了
当然,上述的类型定义是合法的,因为Typescript
不知道V
是一个什么东西,那么此时,我们就可以让Infer
上场了,Infer
的作用是让Typescript
自己帮忙推导类型:
typescript
type Flatterned<T> = T extends Array<infer V> ? V : T;
type Result = Flatterned<number[]>;// number
// 那么如果我们这样使用呢?
type Result = Flatterned<number[][]>;// number[]
由于我们上面的类型定义只是推导了一层,所以最终只会解开一层,所以对于二维数组,只能解析出一维数组,那么,如果我们想要不管都少层,都把它拍平要怎么做呢?如果在写Javascript
程序时,大家应该能够轻易的想到,此处应该使用:递归。但现在是在描述类型呀,还能使用递归么?
Typescript
引擎是及其聪明且为我们考虑的,为了应对这种情况,在定义类型描述时,也可以实现类似递归程序一样的类型描述:
typescript
type Flatterned<T> = T extends Array<infer V> ? Flatterned<V> : T;
type Result = Flatterned<number[]>;// number
// 那么如果我们这样使用呢?
type Result = Flatterned<number[][]>;// number
这样,无论我们嵌套多少层,最终都能将数组内元素的真实类型找出来。
那么,我们回过头来看看下面这个类型定义,究竟干了什么:
typescript
type Flatterned<T> = T extends Array<infer V> ? Flatterned<V> : T;
Flatterned
接收一个泛型参数T
,首先判断这个T
是不是继承于数组的- 如果
T
并非继承于数组,那么我们就直接返回T
类型 - 如果发现
T
是继承于数组的,那么我们需要推导出数组中的元素是什么类型的,我们使用Infer
关键字帮我们推导数组内的元素类型,并暂存在变量V
当中 - 当推导出一层数组元素
V
后,由于我们不太确定V
是否仍是一个数组,因此递归的调用Flatterned<V>
,让其不断地递归推导,知道最终能够确定传入的泛型参数不再是继承自数组时,终止递归,返回最终推导出的类型。
最后,我们言归正传,回过头来看看,我们要如何设计flattern
函数:
typescript
type Flatterned<T> = T extends Array<infer V> ? Flatterned<V> : T;
function flattern<T extends Array<any>>(arr: T): Array<Flatterned<T>> {
return [].concat(...arr.map(item => Array.isArray(item) ? flattern(item) : item))
}
console.log(flattern([[[[[[1, 2], 3]]], 4]]))// [ 1, 2, 3, 4 ]
至此,我们就算是完成了刚开始我们的问题了,接下来,大家再来思考一下这个类型能帮我们解决什么问题:
typescript
type UnWrapped<T> = T extends Promise<infer V> ? UnWrapped<V> : T;
type ResponseData = UnWrapped<Promise<Promise<string>>>;// string;
相信大家已经能够很容易才出来了,这里就是把可能嵌套了很多层的Promise
解开,找出真正的返回体结构,这在我们实际工作当中算是比较常见的。
Inter模块优化
通过上面的示例,我们已经初识了Infer
的用法与作用,但在某些情况下,如果比较复杂的类型当中,使用太多的Infer
可能会导致我们的类型难以阅读,因此,我们要学会对这些类型进行一定的模块优化,例如:
typescript
// 你该如何描述 UnWrapped 类型呢?
type ResponseData = UnWrapped<Promise<string>[]>;// 期望得到:string[];
// 最简单暴力的方法,我们就用两层 infer 来推导
type UnWrapped<T> = T extends (infer V)[] ? V extends Promise<infer U> ? U[] : V : T
type ResponseData = UnWrapped<Promise<string>[]>;// string[]
上面的示例是没问题的,我们能够正常的获取到我们预期的类型string[]
,但由于多个Infer
联用,可能导致其他开发者阅读起来非常困难,我们来看看要如何优化。
其实类型的模块优化跟程序的模块优化道理是一样的,就是把一些共性抽离:
typescript
// 首先抽离出拆解 Promise 的模块
type UnWrappedPromise<T> = T extends Promise<infer V> ? V : T extends (infer U)[] ? UnWrappedArray<T> : T;
// 我们再抽离出拆借 Array 的模块
// 如果是数组的话,将数组中的每一个元素都调用一下提取 Promise 类型
type UnWrappedArray<T> = T extends (infer V)[] ? { [P in keyof T]: UnWrappedPromise<T[P]> } : T
type T = UnWrappedPromise<Promise<string>[]>;// string[]
可以看到,通过拆解共性,我们把原本一个比较复杂的类型描述,按照功能不同拆解成了两个相对独立的类型描述模块,我们既可以单独使用,又可以组合使用,且每个模块都有各自的核心职责,易于阅读和理解。
不过上面的程序,只会去除一层的Promise
,如果我们这么使用呢?
typescript
type T = UnWrappedPromise<Promise<Promise<string>>[]>;// Promise<string>[]
我们可以看到,获得的类型是Promise<string>[]
,那么,如何解开所有的Promise
呢?还是用递归来解决:
typescript
// 首先抽离出拆解 Promise 的模块
// 如果 T 是 Promise 的,我们递归处理一下,解决嵌套多层的 Promise
type UnWrappedPromise<T> = T extends Promise<infer V> ? UnWrappedPromise<V> : T extends (infer U)[] ? UnWrappedArray<T> : T;
// 我们再抽离出拆借 Array 的模块
// 如果是数组的话,将数组中的每一个元素都调用一下提取 Promise 类型
type UnWrappedArray<T> = T extends (infer V)[] ? { [P in keyof T]: UnWrappedPromise<T[P]> } : T
type T = UnWrappedPromise<Promise<Promise<string>>[]>;// string[]
这样我们就可以将所有的Promise
都解开了。
再来看一个例子:
typescript
type RouteDic<Arg extends string> = {
[p in Arg]: string;
};
type RouteParam<Route extends string> = Route extends `${string}:${infer Rest}` ? RouteDic<Rest> : never;
const path = "/page/:id";
const params: RouteParam<typeof path> = {
"id": "11111",// 不报错,且有 key 为 id 的类型提示
"name": "kiner",// 报错,因为在 path 当中不存在 name 参数
};
console.log(params);
结语
至此,相信大家都领略到了Typescript
类型计算体系的强大了。如果能够善用Typescript
的类型计算体系
,可以让我们更加清晰地把控一个复杂的类型系统,让我们的系统更加严谨。