What have I learned from type definitions in React source code?

Today, I looked at the type definition of React, that is, index.d.ts under the @types/react package, and found some interesting ways to write it.

This article will share these writing methods, it is estimated that most people do not know:

Extract the value of an optional index

First, I saw this piece of type logic:

This logic is to take the value of the ref index of the index type, but by pattern matching, the extracted type is returned to the local variable R declared by infer.

Simplify it like this:

Extract the type of the value of the ref index of the Props returned.

I'm thinking, why is it so troublesome, can't you get the value of the ref index directly with Props['ref']?

So I changed it to this:

Then try:

No, this is an optional index, and the value type is a union type containing undefined.

Then Exclude is not enough:

This is also simpler than the infer method. Why does the React type definition use the optional index type taken by infer?

Then I suddenly thought, what if the type of this ref value is undefined?

I tried:

Indeed, there is a problem with what I wrote like that. If the type of the value is originally undefined, the Exclude will be never after the undefined is removed, and other people's methods are fine:

So I add undefined processing:

that's fine.

Compare the following two ways of writing:

Indeed, the React way of writing is more concise.

By the way, what about the judgment at the top?

This judgment is unnecessary. If there is no ref, isn't Props['ref'] just returning never? There is no need to judge separately?

Then I saw this comment:

In ts 3.0, if the index type has no corresponding index, the returned type is {} instead of never.

It turns out that this 'ref' extends keyof Props is for compatibility, not meaningless.

Here are two things I learned from this genre:

  • 索引访问 Obj[Key] 和 infer 提取和都可以取到索引类型的某个索引的值,但是当处理可选索引的时候,用 infer 更简洁一些,因为前者要取出类型之后再单独处理下 undefined,而后者在 infer 的时候就顺便处理了 undefined。

  • ts 3.0 中如果索引类型没有对应的索引,返回的是 {} 不是 never,如果对兼容性要求高的话,可以用 'xx' in keyOf Obj 的方式做下兼容

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

索引类型和 any、never 的处理

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

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

看下结果:

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

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

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

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

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

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

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

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

Pick

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

type Pick<T, K extends keyof T> = { [P in K]: T[P]; };
复制代码

测试下:

Partial

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

type Partial<T> = { [P in keyof T]?: T[P]; };
复制代码

测试下:

Extract

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

type Extract<T, U> = T extends U ? T : never;
复制代码

测试下:

Exclude

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

type Extract<T, U> = T extends U ? T : never;
复制代码

测试下:

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

只有 A 中有的就保留的逻辑是: Pick<A, Exclude<keyof A, keyof B>>

A、B 都有的变为可选: Partial<Pick<A, Extract<keyof A, keyof B>>>

B 中有但 A 中没有的也变为可选:Partial<Pick<B, Exclude<keyof B, keyof A>>>

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

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

那前面那两个判断是啥?

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

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

比如这样一个联合类型:

type Union = 'a' | 'b' | 'c';
复制代码

我们想把其中的 a 大写,就可以这样写:

type UppercaseA<Item extends string> = 
    Item extends 'a' ?  Uppercase<Item> : Item;
复制代码

因为 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<Obj[Key], undefined> 去掉类型中的 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 类型定义做的挺完善的,考虑到了各种类型的处理,也考虑到了低版本的兼容,从中还是能学到不少东西的。

Guess you like

Origin juejin.im/post/7079449083919728671