|
| 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