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

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

相关推荐
下雪天的夏风1 小时前
TS - tsconfig.json 和 tsconfig.node.json 的关系,如何在TS 中使用 JS 不报错
前端·javascript·typescript
天下无贼!15 小时前
2024年最新版TypeScript学习笔记——泛型、接口、枚举、自定义类型等知识点
前端·javascript·vue.js·笔记·学习·typescript·html
Jorah3 天前
1. TypeScript基本语法
javascript·ubuntu·typescript
小白小白从不日白3 天前
TS axios封装
前端·typescript
aimmon4 天前
Superset二次开发之源码DependencyList.tsx 分析
前端·typescript·二次开发·bi·superset
下雪天的夏风4 天前
Vant 按需引入导致 Typescript,eslint 报错问题
前端·typescript·eslint
theMuseCatcher5 天前
Vue3+TypeScript+Vite+Less 开发 H5 项目(amfe-flexible + postcss-pxtorem)
typescript·less·postcss
Qiyandays5 天前
vue + Lodop 制作可视化设计页面 实现打印设计功能(四)
前端·vue.js·typescript
人工智能的苟富贵5 天前
微信小程序中的模块化、组件化开发:完整指南
微信小程序·小程序·typescript
Code blocks8 天前
小试牛刀-区块链Solana多签账户
算法·typescript·区块链·github