探秘Typescript·Inter和强大的类型计算体系

探秘Typescript·Infer与强大的类型计算体系

探秘Typescript·Inter和强大的类型计算体系

常见的类型计算场景

联合类型 |

我们之前学习过联合类型,使用|符号可以将多个独立的类型联合成一个新的类型,如:

type Shape = Circle | Square | Rect;
// 如上述的 Shape 类型就是将 `Circle`、`Square`和`Rect`三种形状的的联合

交叉类型 &

除了联合类型外,我们还可以用交叉类型将多个类型合并成一个新的类型,如:

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函数,这个函数可以将传入的任意层级的数组拍平成一维数组,如:

flattern([[[[[[[], 1, 2]]]]]]);// 最终返回为:[1, 2]

那么这个函数我们要如何进行类型定义呢?或许大家觉得可以这样:

function flattern(arr: Array<any>): Array<any> {
  
}

上述的类型描述虽然不会报错,但这样就是去了对数组内元素类型的校验了。

那么,我们根据类型计算再来重新设计一下:

type Flatterned<T> = T extends Array<V> ? V : T;
type Result = Flatterned<number[]>
// 先不考虑上面的类型定义的合法性,至少从语义上来说,我们拍平的结果类型就是 number 类型的,这样我们就可以把数组中的数组项的类型给抽离出来了

当然,上述的类型定义是合法的,因为Typescript不知道V是一个什么东西,那么此时,我们就可以让Infer上场了,Infer的作用是让Typescript自己帮忙推导类型:

type Flatterned<T> = T extends Array<infer V> ? V : T;
type Result = Flatterned<number[]>;// number

// 那么如果我们这样使用呢?
type Result = Flatterned<number[][]>;// number[]

由于我们上面的类型定义只是推导了一层,所以最终只会解开一层,所以对于二维数组,只能解析出一维数组,那么,如果我们想要不管都少层,都把它拍平要怎么做呢?如果在写Javascript程序时,大家应该能够轻易的想到,此处应该使用:递归。但现在是在描述类型呀,还能使用递归么?

Typescript引擎是及其聪明且为我们考虑的,为了应对这种情况,在定义类型描述时,也可以实现类似递归程序一样的类型描述:

type Flatterned<T> = T extends Array<infer V> ? Flatterned<V> : T;
type Result = Flatterned<number[]>;// number

// 那么如果我们这样使用呢?
type Result = Flatterned<number[][]>;// number

这样,无论我们嵌套多少层,最终都能将数组内元素的真实类型找出来。

那么,我们回过头来看看下面这个类型定义,究竟干了什么:

type Flatterned<T> = T extends Array<infer V> ? Flatterned<V> : T;
  • Flatterned接收一个泛型参数T,首先判断这个T是不是继承于数组的
  • 如果T并非继承于数组,那么我们就直接返回T类型
  • 如果发现T是继承于数组的,那么我们需要推导出数组中的元素是什么类型的,我们使用Infer关键字帮我们推导数组内的元素类型,并暂存在变量V当中
  • 当推导出一层数组元素V后,由于我们不太确定V是否仍是一个数组,因此递归的调用Flatterned<V>,让其不断地递归推导,知道最终能够确定传入的泛型参数不再是继承自数组时,终止递归,返回最终推导出的类型。

最后,我们言归正传,回过头来看看,我们要如何设计flattern函数:

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 ]

至此,我们就算是完成了刚开始我们的问题了,接下来,大家再来思考一下这个类型能帮我们解决什么问题:

type UnWrapped<T> = T extends Promise<infer V> ? UnWrapped<V> : T;
type ResponseData = UnWrapped<Promise<Promise<string>>>;// string;

相信大家已经能够很容易才出来了,这里就是把可能嵌套了很多层的Promise解开,找出真正的返回体结构,这在我们实际工作当中算是比较常见的。

Inter模块优化

通过上面的示例,我们已经初识了Infer的用法与作用,但在某些情况下,如果比较复杂的类型当中,使用太多的Infer可能会导致我们的类型难以阅读,因此,我们要学会对这些类型进行一定的模块优化,例如:

// 你该如何描述 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联用,可能导致其他开发者阅读起来非常困难,我们来看看要如何优化。

其实类型的模块优化跟程序的模块优化道理是一样的,就是把一些共性抽离:

// 首先抽离出拆解 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,如果我们这么使用呢?

type T = UnWrappedPromise<Promise<Promise<string>>[]>;// Promise<string>[]

我们可以看到,获得的类型是Promise<string>[],那么,如何解开所有的Promise呢?还是用递归来解决:

// 首先抽离出拆解 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都解开了。

再来看一个例子:


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类型计算体系,可以让我们更加清晰地把控一个复杂的类型系统,让我们的系统更加严谨。

原文链接:https://juejin.cn/post/7327562275107209231 作者:kinertang

(0)
上一篇 2024年1月25日 上午10:43
下一篇 2024年1月25日 上午10:53

相关推荐

发表回复

登录后才能评论