作者:神说要有光

React 类型定义做的挺完善的,考虑到了各种类型的处理,也考虑到了低版本的兼容,从中还是能学到不少东西的。

打开网易新闻 查看更多图片

今天看了下 React 的类型定义,也就是 @types/react 包下的 index.d.ts,发现了一些有趣的写法。

这篇文章就分享下这些写法,估计大部分人都不知道:

提取可选索引的值

首先,我看到了这样一段类型逻辑:

这段逻辑就是取索引类型的 ref 索引的值,但是是通过模式匹配的方式,把提取的类型放到 infer 声明的局部变量 R 里返回的。

简化一下就是这样的:

打开网易新闻 查看更多图片

提取 Props 的 ref 索引的值的类型返回。

我在想,这么麻烦干什么,直接 Props['ref'] 不就能拿到 ref 索引的值么?

于是我就改成了这样:

然后试了下:

不对呀,人家这是可选索引,值的类型是包含 undefined 的联合类型。

那就 Exclude 下不就行了:

打开网易新闻 查看更多图片

这样也比那个 infer 的方式简洁呀,为啥 React 类型定义都是用的 infer 取的可选索引的类型呢?

后来我突然想到,如果这个 ref 值的类型就是 undefined 呢?

我试了下:

确实,我那样写是有问题的,如果值的类型本来就是 undefined,Exclude 掉 undefined 后就是 never 了,而人家那种方式就没问题:

于是我就加一下 undefined 的处理:

这样就行了。

对比了下两种写法:

打开网易新闻 查看更多图片

确实还是 React 的那种写法更简洁。

对了,那上面那层判断呢?

这个判断没必要的吧,如果没有 ref,那 Props['ref'] 不就是返回 never 么,没必要单独判断呀?

然后我就看到了这样一段注释:

在 ts 3.0 中,如果索引类型没有对应的索引,那返回的类型是 {} 而不是 never。

原来如此,这个 'ref' extends keyof Props 是为了做兼容的呀,不是没意义。

这就是我从这个类型中学到的两个知识点:

  • 索引访问 Obj[Key] 和 infer 提取和都可以取到索引类型的某个索引的值,但是当处理可选索引的时候,用 infer 更简洁一些,因为前者要取出类型之后再单独处理下 undefined,而后者在 infer 的时候就顺便处理了 undefined。
  • ts 3.0 中如果索引类型没有对应的索引,返回的是 {} 不是 never,如果对兼容性要求高的话,可以用 'xx' in keyOf Obj 的方式做下兼容。

我们从这个类型里学到了不少东西,再来看下第二个类型:

索引类型和 any、never 的处理

然后我又看到了这样一个类型,

先试一下它的功能,传入两个索引类型:

看下结果:

这是些啥啊,谁能看得懂呀。

其实这只是因为 TS 没有计算出最终的类型而已,用到的时候才会计算,所以我们可以这样处理下:

Copy 的高级类型是通过映射类型的语法构造了一个新的索引类型,它的功能是原封不动的复制一个索引类型。

类型参数 Obj 约束为索引类型,也就是 Record。key 保持不变,也就是 Key in keyof Obj,value 也保持不变,也就是 Obj[Key]。

因为重新生成的类型的过程中要做计算,所以那个类型就能提示出最终的结果了:

所以说,这个类型的作用是两个索引类型 A,B,只有 A 中有的就保留,A、B 都有的变为可选,B 有但 A 没有的变为可选。

那这段逻辑具体是怎么用 TS 实现的呢?

我们先来过一下 TS 这些内置的高级类型:

Pick

Pick 的作用是通过映射类型的语法构造一个新的索引类型,根据传入的 Key 对索引做下过滤:

type Pick

= { [P in K]: T[P]; };1.

测试下:

Partial

Partial 也是通过映射类型的语法构造一个新的索引类型,但是会把索引变为可选:

type Partial

= { [P in keyof T]?: T[P]; };1.

测试下:

Extract

Extract 是取两个联合类型都包含的部分,也就是取交集:

type Extract = T extends U ? T : never;1.

测试下:

Exclude

Exclude 是从联合类型 A 中去掉联合类型 B 中的类型,也就是取差集:

type Extract = T extends U ? T : never;1.

测试下:

学会了用 Pick、Partial、Exclude、Extract 这些高级类型,那上面的那段逻辑我们就知道怎么实现了:

只有 A 中有的就保留的逻辑是:Pick>。

A、B 都有的变为可选:Partial>>。

B 中有但 A 中没有的也变为可选:Partial>>。

这样,这个类型的主要逻辑我们就理清了:

把三部分计算结果的索引类型取交叉类型,就会合并到一起。

那前面那两个判断是啥?

P extends any 还有这个 string extends keyof P,这俩都是做啥的?

P extends any 这个是因为联合类型当作为类型参数出现在条件类型左边时,会把每个类型单独传入做计算,最后把计算结果合并成联合类型,这个特性叫做分布式条件类型。

因为 Item 是类型参数,出现在了条件类型的左边,而且传入的是联合类型,那就触发分发特性,会把每个类型单独传入做计算,然后把结果合并成联合类型。

所以这里的 P extends any 的作用就是触发联合类型特性的,从而让这个类型能正确处理联合类型。不然联合类型整个传入的话,后面怎么做计算。

这里的 P extends any 换成 P extends P 也可以,都是一样的作用。

那后面那段代码 string extends keyof P 是啥意思?

这个我确实想了一段时间,如果 { a: 1, b: 2} 这样的索引类型,keyof 的结果是 'a' | 'b',而如果是数组类型,那 keyof 的结果是 0 | 1 | 'length' | 'toString' | ...

什么类型的 keyof 结果是 string 呢?

突然,我想起了前几天学到的一个知识点:用 keyof any 代替 string | number | symbol 更灵活:

而且我试了下 never 的 keyof 结果也是这个:

所以说 string extends keyof P 就可以排除 any 和 never 的情况!

妙呀,还能这么区分 any 和 never。

所以说,这个类型的逻辑我们已经理清了:

这个类型的功能是保留只有 A 有的索引,把 A、B 都有的索引变为可选,把只有 B 有的索引变为可选。

而且处理了联合类型的情况。

如果传入的是 any 或者 never,不做处理,直接返回。

这个类型里我们也学到了不少东西。

总结

我看了下 @types/react 的类型定义,学到了不少东西:

  • 可选索引的值的提取,用 infer 比 Obj[key] 更方便,因为前者只需要 Obj[Key] extends { xxx?: infer Value: undefined},而后者需要先排除值的类型就是 undefined 的情况,然后再用 Exclude 去掉类型中的 undefined。
  • ts 3.0 中取索引类型没有的索引会返回 {} 而不是 never,需要兼容的话可以单独做下判断:'xxx' in keyof Obj。
  • 处理索引类型可以综合用 Pick、Partial、Exclude、Extract 等内置高级类型对每一部分索引做处理,然后取交叉类型来合并到一起。
  • P extends any 和 P extends P 的作用是触发联合类型的分发特性的,加上这段处理才能正确处理联合类型,会把每个类型单独传入做计算,最后把结果合并成联合类型。
  • string extends keyof Obj 可以判断出 any 和 never 类型,只有这两种类型取 keyof 的结果是 string | nubmer | symbole,包含 string。

而且,还讲了一个小技巧:

ts 类型只有计算的时候才会求值,如果是索引类型,可以用映射类型的语法创建个一摸一样的索引类型,因为用到了,就会做计算,从而就可以显示出最终的类型。

不得不说,React 类型定义做的挺完善的,考虑到了各种类型的处理,也考虑到了低版本的兼容,从中还是能学到不少东西的。

来源: 神光的编程秘籍