LOG // 046 · TypeScript

TypeScript 的 infer 到底在推什么

结论先行把 infer R 读成「这里有个类型我不知道,先叫它 R,推出来再说」, 90% 的困惑会消失。它不是关键字魔法,只是条件类型里的一次模式匹配。

我卡在 infer 上大概有半年。看别人写的工具类型能看懂,自己写就报错。 后来发现问题是:我一直把它当成「返回值」,其实它是「变量名」。

01infer 就是声明变量

条件类型的形式是 T extends P ? A : B。infer 只能写在 P 的位置,作用是把匹配到的那一部分绑定到一个名字上, 然后在 A 里用这个名字。

types/infer-demo.tsts
1type Unpack<T> = T extends Array<infer Item> ? Item : T;
2//                              ^^^^^^^^^^ 声明:匹配的项叫 Item
3
4type A = Unpack<string[]>;  // string
5type B = Unpack<number>;    // number(没匹配上,走 false 分支)

02第一个例子:ReturnType

标准库里最经典的一个,看懂它基本就懂了一半。

lib.es5.d.ts(简化)ts
1type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;
2
3const load = (id: string) => ({ id, name: 'x' });
4type Loaded = ReturnType<typeof load>;  // { id: string; name: string }
Tip

注意 infer R 只覆盖返回值位置。想同时拿参数,就在参数位置也写一个 infer——它们互不冲突,因为是两次独立的匹配。

03第二个例子:参数与元组

types/infer-demo.tsts
1type Params<T> = T extends (...args: infer P) => any ? P : never;
2
3type P1 = Params<(a: string, b: number) => void>;  // [string, number]
4
5// 只取第一个参数
6type First<T> = T extends (first: infer F, ...rest: any[]) => any ? F : never;
7type F1 = First<(a: string, b: number) => void>;   // string

04递归:把字符串拆开

条件类型可以引用自己,于是 infer 就变成了一个逐字符的解析器。

types/infer-demo.tsts
1type Split<S extends string> =
2  S extends `${infer Head}${infer Tail}` ? [Head, ...Split<Tail>] : [];
3
4type Chars = Split<'abc'>;  // ['a', 'b', 'c']
Warn

递归深度有上限(大约 50 层)。真在生产里拆长字符串,先想清楚是不是该换个方案。

05什么时候不该用

  • 能用泛型参数解决的,别用 infer。多一层条件类型就多一层报错噪音。
  • 写完自己读一遍报错信息。推不出来时 TS 会把整个条件类型摊开打印,那是最真实的成本。

下一篇写泛型层级怎么砍——这篇里那些类型如果放到真实项目,报错信息会有多难看。