Infer 关键字用于条件中的类型推导。

Typescript 官网也拿 `ReturnType` 这一经典例子说明它的作用：

```typescript
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;
```

理解为：如果 `T` 继承了 `(...args: any[]) => any` 类型，则返回类型 `R`，否则返回 `any`。其中 `R` 是什么呢？`R` 被定义在 `extends (...args: any[]) => infer R` 中，即 R 是从传入参数类型中推导出来的。

## 精读

我们可以从两个视角来理解 `infer`，分别是需求角度与设计角度。

### 需求角度理解 infer

实现 `infer` 这个关键字一定是背后存在需求，这个需求是普通 Typescript 能力无法满足的。

设想这样一个场景：实现一个函数，接收一个数组，返回第一项。

我们无法用泛型来描述这种类型推导，因为泛型类型是一个整体，而我们想要返回的是入参其中某一项，我们并不能通过类似 `T[0]` 的写法拿到第一项类型：

```typescript
function xxx<T>(...args: T[]): T[0]
```

而实际上不支持这种写法也是合理的，因为这次是获取第一项类型，如果 `T` 是一个对象，我们想返回其中 `onChange` 这个 Key 的返回值类型，就不知道如何书写了。所以此时必须用一种新的语法实现，就是 `infer`。

### 设计角度理解 infer

从类型推导功能来看，泛型功能非常强大，我们可以用泛型描述调用时才传入的类型，并提前将它描述在类型表达式中：

```typescript
function xxx<T>(value: T): { result: T }
```

但我们发现 `T` 这个泛型太整体化了，我们还不具备从中 Pick 子类型的能力。也就是对于 `xxx<{label: string}>` 这个场景，`T = {label: string}`，但我们无法将 `R` 定义为 `{label: R}` 这个位置，因为泛型是一个不可拆分的整体。

而且实际上为了类型安全，我们也不能允许用户描述任意的类型位置，**万一传入的类型结构不是 `{label: xxx}` 而是一个回调 `() => void`，那子类型推导岂不是建立在了错误的环境中。** 所以考虑到想要拿到 `{label: infer R}`，首先参数必须具备 `{label: xxx}` 的结构，所以正好可以将 `infer` 与条件判断 `T extends xxx ? A : B` 结合起来用，即：

```typescript
type GetLabelTypeFromObject<T> = T extends { label: infer R } ? R : never

type Result = GetLabelTypeFromObject<{ label: string }>;
// type Result = string
```

即如果 `T` 遵循 `{ label: any }` 这样一个结构，那么我可以将这个结构中任何变量位置替换为 `infer xxx`，如果传入类型满足这个结构（TS 静态解析环节判断），则可以基于这个结构体继续推导，所以在推导过程中我们就可以使用 `infer xxx` 推断的变量类型。

回过头来看第一个需求，拿到第一个参数类型就可以用 `infer` 实现了：

```typescript
type GetFirstParamType<T> = T extends (...args: infer R) => any ? R[0] : never
```

可以理解为，如果此时 `T` 满足 `(...args: any) => any` 这个结构，同时我们用 `infer R` 表示 `R` 这个临时变量指代第一个 `any` 运行时类型，那么整个函数返回的类型就是 `R`。如果 `T` 都不满足 `(...args: any) => any` 这个结构，比如 `GetFirstParamType<number>`，那这种推导根本无从谈起，直接返回 `never` 类型兜底，当然也可以自定义比如 `any` 之类的任何类型。

## 概述

我们理解了 `infer` 含义后，再结合 [conditional infer](https://learntypescript.dev/09/l2-conditional-infer) 这篇文章理解里面的例子，有助于加深记忆。

```typescript
type ArrayElementType<T> = T extends (infer E)[] ? E : T;
// type of item1 is `number`
type item1 = ArrayElementType<number[]>;
// type of item1 is `{name: string}`
type item2 = ArrayElementType<{ name: string }>;
```

可以看到，`ArrayElementType` 利用了条件推断与 `infer`，表示了这样一个逻辑：如果 `T` 类型是一个数组，且我们将数组的每一项定义为 `E` 类型，那么返回类型就为 `E`，否则为 `T` 整体类型本身。

所以对于 `item1` 是满足结构的，所以返回 `number`，而 `item2` 不满足结构，所以返回其类型本身。

特别补充一点，对于下面的例子返回什么呢？

```typescript
type item3 = ArrayElementType<[number, string]>;
```

答案是 `number | string`，原因是我们用多个 `infer E`（`(infer E)[]` 相当于 `[infer E, infer E]...` 不就是多个变量指向同一个类型代词 `E` 嘛）同时接收到了 `number` 和 `string`，所以可以理解为 `E` 时而为 `number` 时而为 `string`，所以是或关系，这就是协变。

那如果是函数参数呢？

```typescript
type Bar<T> = T extends { a: (x: infer U) => void; b: (x: infer U) => void }
  ? U : never
type T21 = Bar<{ a: (x: string) => void; b: (x: number) => void }>; // string & number
```

发现结果是 `string & number`，也就是逆变。但这个例子也是同一个 `U` 时而为 `string` 时而为 `number` 呀，为什么是且的关系，而不是或呢？

其实协变或逆变与 `infer` 参数位置有关。在 TypeScript 中，对象、类、数组和函数的返回值类型都是协变关系，而函数的参数类型是逆变关系，所以 `infer` 位置如果在函数参数上，就会遵循逆变原则。

> 逆变与协变：
>
> - 协变(co-variant)：类型收敛。
> - 逆变(contra-variant)：类型发散。

关于逆变与协变更深入的话题可以再开一篇文章了，这里就不细讲了，对于 `infer` 理解到这里就够啦。

## 总结

`infer` 关键字让我们拥有深入展开泛型的结构，并 Pick 出其中任何位置的类型，并作为临时变量用于最终返回类型的能力。

对于 Typescript 类型编程，最大的问题莫过于希望实现一个效果却不知道用什么语法，`infer` 作为一个强大的类型推导关键字，势必会在大部分复杂类型推导场景下派上用场，所以在遇到困难时，可以想想是不是能用 `infer` 解决问题。

> 讨论地址是：[精读《Typescript infer 关键字》· Issue #346 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/346)

**如果你想参与讨论，请 [点击这里](https://github.com/dt-fe/weekly)，每周都有新的主题，周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**

> 关注 **前端精读微信公众号**

<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">

> 版权声明：自由转载-非商用-非衍生-保持署名（[创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh)）
