Skip to content

Commit 1cf5e33

Browse files
committed
207
1 parent 4ac6750 commit 1cf5e33

2 files changed

Lines changed: 117 additions & 1 deletion

File tree

readme.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
前端界的好文精读,每周更新!
88

9-
最新精读:<a href="./前沿技术/206.%E7%B2%BE%E8%AF%BB%E3%80%8A%E4%B8%80%E7%A7%8D%20Hooks%20%E6%95%B0%E6%8D%AE%E6%B5%81%E7%AE%A1%E7%90%86%E6%96%B9%E6%A1%88%E3%80%8B.md">206.精读《一种 Hooks 数据流管理方案》</a>
9+
最新精读:<a href="./前沿技术/207.%E7%B2%BE%E8%AF%BB%E3%80%8ATypescript%20infer%20%E5%85%B3%E9%94%AE%E5%AD%97%E3%80%8B.md">207.精读《Typescript infer 关键字》</a>
1010

1111
素材来源:[周刊参考池](https://github.com/ascoders/weekly/issues/2)
1212

@@ -164,6 +164,7 @@
164164
- <a href="./前沿技术/204.%E7%B2%BE%E8%AF%BB%E3%80%8A%E9%BB%98%E8%AE%A4%E3%80%81%E5%91%BD%E5%90%8D%E5%AF%BC%E5%87%BA%E7%9A%84%E5%8C%BA%E5%88%AB%E3%80%8B.md">204.精读《默认、命名导出的区别》</a>
165165
- <a href="./前沿技术/205.%E7%B2%BE%E8%AF%BB%E3%80%8AJS%20with%20%E8%AF%AD%E6%B3%95%E3%80%8B.md">205.精读《JS with 语法》</a>
166166
- <a href="./前沿技术/206.%E7%B2%BE%E8%AF%BB%E3%80%8A%E4%B8%80%E7%A7%8D%20Hooks%20%E6%95%B0%E6%8D%AE%E6%B5%81%E7%AE%A1%E7%90%86%E6%96%B9%E6%A1%88%E3%80%8B.md">206.精读《一种 Hooks 数据流管理方案》</a>
167+
- <a href="./前沿技术/207.%E7%B2%BE%E8%AF%BB%E3%80%8ATypescript%20infer%20%E5%85%B3%E9%94%AE%E5%AD%97%E3%80%8B.md">207.精读《Typescript infer 关键字》</a>
167168

168169
### 设计模式
169170

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
Infer 关键字用于条件中的类型推导。
2+
3+
Typescript 官网也拿 `ReturnType` 这一经典例子说明它的作用:
4+
5+
```typescript
6+
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;
7+
```
8+
9+
理解为:如果 `T` 继承了 `extends (...args: any[]) => any` 类型,则返回类型 `R`,否则返回 `any`。其中 `R` 是什么呢?`R` 被定义在 `extends (...args: any[]) => infer R` 中,即 R 是从传入参数类型中推导出来的。
10+
11+
## 精读
12+
13+
我们可以从两个视角来理解 `infer`,分别是需求角度与设计角度。
14+
15+
### 需求角度理解 infer
16+
17+
实现 `infer` 这个关键字一定是背后存在需求,这个需求是普通 Typescript 能力无法满足的。
18+
19+
设想这样一个场景:实现一个函数,接收一个数组,返回第一项。
20+
21+
我们无法用泛型来描述这种类型推导,因为泛型类型是一个整体,而我们想要返回的是入参其中某一项,我们并不能通过类似 `T[0]` 的写法拿到第一项类型:
22+
23+
```typescript
24+
function xxx<T>(...args: T[]): T[0]
25+
```
26+
27+
而实际上不支持这种写法也是合理的,因为这次是获取第一项类型,如果 `T` 是一个对象,我们想返回其中 `onChange` 这个 Key 的返回值类型,就不知道如何书写了。所以此时必须用一种新的语法实现,就是 `infer`
28+
29+
### 设计角度理解 infer
30+
31+
从类型推导功能来看,泛型功能非常强大,我们可以用泛型描述调用时才传入的类型,并提前将它描述在类型表达式中:
32+
33+
```typescript
34+
function xxx<T>(value: T): { result: T }
35+
```
36+
37+
但我们发现 `T` 这个泛型太整体化了,我们还不具备从中 Pick 子类型的能力。也就是对于 `xxx<{label: string}>` 这个场景,`T = {label: string}`,但我们无法将 `R` 定义为 `{label: R}` 这个位置,因为泛型是一个不可拆分的整体。
38+
39+
而且实际上为了类型安全,我们也不能允许用户描述任意的类型位置,**万一传入的类型结构不是 `{label: xxx}` 而是一个回调 `() => void`,那子类型推导岂不是建立在了错误的环境中。** 所以考虑到想要拿到 `{label: infer R}`,首先参数必须具备 `{label: xxx}` 的结构,所以正好可以将 `infer` 与条件判断 `T extends ? A : B` 结合起来用,即:
40+
41+
```typescript
42+
type GetLabelTypeFromObject<T> = T extends ? { label: infer R } ? R : never
43+
44+
type Result = GetLabelTypeFromObject<{ label: string }>;
45+
// type Result = string
46+
```
47+
48+
即如果 `T` 遵循 `{ label: any }` 这样一个结构,那么我可以将这个结构中任何变量位置替换为 `infer xxx`,如果传入类型满足这个结构(TS 静态解析环节判断),则可以基于这个结构体继续推导,所以在推导过程中我们就可以使用 `infer xxx` 推断的变量类型。
49+
50+
回过头来看第一个需求,拿到第一个参数类型就可以用 `infer` 实现了:
51+
52+
```typescript
53+
type GetFirstParamType<T> = T extends ? (...args: infer R) => any ? R[0] : never
54+
```
55+
56+
可以理解为,如果此时 `T` 满足 `(...args: any) => any` 这个结构,同时我们用 `infer R` 表示 `R` 这个临时变量指代第一个 `any` 运行时类型,那么整个函数返回的类型就是 `R`。如果 `T` 都不满足 `(...args: any) => any` 这个结构,比如 `GetFirstParamType<number>`,那这种推导根本无从谈起,直接返回 `never` 类型兜底,当然也可以自定义比如 `any` 之类的任何类型。
57+
58+
## 概述
59+
60+
我们理解了 `infer` 含义后,再结合 [conditional infer](https://learntypescript.dev/09/l2-conditional-infer) 这篇文章理解里面的例子,有助于加深记忆。
61+
62+
```typescript
63+
type ArrayElementType<T> = T extends (infer E)[] ? E : T;
64+
// type of item1 is `number`
65+
type item1 = ArrayElementType<number[]>;
66+
// type of item1 is `{name: string}`
67+
type item2 = ArrayElementType<{ name: string }>;
68+
```
69+
70+
可以看到,`ArrayElementType` 利用了条件推断与 `infer`,表示了这样一个逻辑:如果 `T` 类型是一个数组,且我们将数组的每一项定义为 `E` 类型,那么返回类型就为 `E`,否则为 `T` 整体类型本身。
71+
72+
所以对于 `item1` 是满足结构的,所以返回 `number`,而 `item2` 不满足结构,所以返回其类型本身。
73+
74+
特别补充一点,对于下面的例子返回什么呢?
75+
76+
```typescript
77+
type item3 = ArrayElementType<[number, string]>;
78+
```
79+
80+
答案是 `number | string`,原因是我们用多个 `infer E``(infer E)[]` 相当于 `[infer E, infer E]...` 不就是多个变量指向同一个类型代词 `E` 嘛)同时接收到了 `number``string`,所以可以理解为 `E` 时而为 `number` 时而为 `string`,所以是或关系,这就是协变。
81+
82+
那如果是函数参数呢?
83+
84+
```typescript
85+
type Bar<T> = T extends { a: (x: infer U) => void; b: (x: infer U) => void }
86+
? U : never
87+
type T21 = Bar<{ a: (x: string) => void; b: (x: number) => void }>; // string & number
88+
```
89+
90+
发现结果是 `string & number`,也就是逆变。但这个例子也是同一个 `U` 时而为 `string` 时而为 `number` 呀,为什么是且的关系,而不是或呢?
91+
92+
其实协变或逆变与 `infer` 参数位置有关。在 TypeScript 中,对象、类、数组和函数的返回值类型都是协变关系,而函数的参数类型是逆变关系,所以 `infer` 位置如果在函数参数上,就会遵循逆变原则。
93+
94+
> 逆变与协变:
95+
>
96+
> - 协变(co-variant):类型收敛。
97+
> - 逆变(contra-variant):类型发散。
98+
99+
关于逆变与协变更深入的话题可以再开一篇文章了,这里就不细讲了,对于 `infer` 理解到这里就够啦。
100+
101+
## 总结
102+
103+
`infer` 关键字让我们拥有深入展开泛型的结构,并 Pick 出其中任何位置的类型,并作为临时变量用于最终返回类型的能力。
104+
105+
对于 Typescript 类型编程,最大的问题莫过于希望实现一个效果却不知道用什么语法,`infer` 作为一个强大的类型推导关键字,势必会在大部分复杂类型推导场景下派上用场,所以在遇到困难时,可以想想是不是能用 `infer` 解决问题。
106+
107+
> 讨论地址是:[精读《Typescript infer 关键字》· Issue #346 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/346)
108+
109+
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
110+
111+
> 关注 **前端精读微信公众号**
112+
113+
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
114+
115+
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))

0 commit comments

Comments
 (0)