TypeScript 进阶|冷门知识点 | 很多人学完
T extends U ? X : Y就觉得条件类型不过如此,直到遇到infer------这个能从类型里「提取」出子类型的黑魔法,才是 TS 类型系统的真正天花板。
问题场景
看看这段代码,你能说出 ReturnType<T> 是怎么实现的吗?
ts
// 这个内置工具类型是怎么知道函数返回值的?
type Result = ReturnType<() => string>; // string
// 再看看这个------从 Promise 里掏出泛型参数
type Inner = Awaited<Promise<number>>; // number
// 实战中更头疼的场景:
// 1. 从一个复杂的 API 响应类型里掏出 data 字段的类型
// 2. 从高阶函数的参数里提取回调的参数类型
// 3. 从数组类型里提取元素类型
大多数人能写出 type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never 吗?写不出来也没关系------这就是 infer 的精髓。
原因分析
条件类型(T extends U ? X : Y)本身不复杂:判断类型是否兼容,然后走不同分支。
但 infer 的威力在于------它可以在 extends 的匹配过程中声明一个「占位变量」,然后让 TypeScript 自动推导出这个位置上的实际类型。
ts
// 基础语法:在 extends 的 U 中声明 infer 变量
type Example<T> = T extends SomeType<infer P> ? P : never;
// ↑ 声明变量 P
// TypeScript 会推断 P = 实际类型
很多人在这一步就卡住了,因为 infer 只能出现在条件类型的 extends 子句中,不能单独使用。而且 infer 像是「反向推导」------你告诉 TS "这里有个类型,你帮我填上"。
解决方案(5 个实战模式)
模式 1:从函数类型提取参数和返回值
ts
// 提取返回值类型(内置 ReturnType 的实现)
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
// 提取第一个参数类型
type FirstArg<T> = T extends (arg: infer A, ...rest: any[]) => any ? A : never;
// 实战:从事件处理函数中推断事件类型
type InferEventType<T> = T extends (e: infer E) => void ? E : never;
// 使用
type ClickHandler = (e: MouseEvent) => void;
type Event = InferEventType<ClickHandler>; // MouseEvent ✅
注意陷阱 :当函数有重载时,infer 只会匹配最后一个重载签名。
模式 2:从 Promise 中提取内部类型
ts
// 内置 Awaited 的简化版
type MyAwaited<T> = T extends Promise<infer U> ? U : T;
// 但 Promise 可能嵌套!真正的 Awaited 是递归的
type DeepAwaited<T> = T extends Promise<infer U> ? DeepAwaited<U> : T;
type T1 = DeepAwaited<Promise<Promise<string>>>; // string ✅
type T2 = DeepAwaited<Promise<number[]>>; // number[] ✅
实战场景 :当你的 API 封装返回 Promise<ApiResponse<Data>> 时,可以直接提取 Data 类型:
ts
type UnwrapApi<T> = T extends Promise<ApiResponse<infer D>> ? D : never;
模式 3:从数组/元组中提取元素类型
ts
// 提取数组元素类型
type ElementType<T> = T extends (infer U)[] ? U : never;
type StrArr = string[];
type El = ElementType<StrArr>; // string ✅
// 提取元组第一个元素
type First<T extends any[]> = T extends [infer F, ...any[]] ? F : never;
type F = First<[number, string, boolean]>; // number ✅
// 提取元组最后一个元素
type Last<T extends any[]> = T extends [...any[], infer L] ? L : never;
type L = Last<[number, string, boolean]>; // boolean ✅
// 提取元组中间的元素
type Middle<T extends any[]> = T extends [any, infer M, any] ? M : never;
type M = Middle<[string, number, boolean]>; // number ✅
实战:从 useState 返回值推导出状态类型
ts
// 比如 React 的 useState 返回 [state, dispatch]
// 我们可以提取 state 的类型
type ExtractState<T> = T extends [infer S, ...any[]] ? S : never;
// 在实际代码中,你可以在泛型组件里这样用:
function useTypedState<T extends [any, ...any[]]>() {
// ...
}
模式 4:在模板字面量类型中提取
这是 TS 4.1+ 带来的黑科技------infer 可以和模板字面量类型结合:
ts
// 从路由路径中提取参数
type ExtractParams<T> = T extends `${string}:${infer Param}/${infer Rest}`
? Param | ExtractParams<Rest>
: T extends `${string}:${infer Param}`
? Param
: never;
type Route = '/user/:id/post/:postId';
type Params = ExtractParams<Route>; // "id" | "postId" ✅
// 从 API 路径中提取资源名称
type ExtractResource<T> = T extends `/api/${infer R}`
? R extends `${infer Name}/${string}`
? Name
: R
: never;
type Res = ExtractResource<'/api/users/123'>; // "users" ✅
实战场景:用模板字面量 + infer 实现类型安全的 API 路由定义:
ts
// 定义类型安全的接口路径
type ApiPaths = '/api/users' | '/api/users/:id' | '/api/posts/:postId';
// 解析出所有参数名
type AllParams = ExtractParams<ApiPaths>; // "id" | "postId"
模式 5:分布式条件类型 + infer 的「组合拳」
当条件类型遇到联合类型时,会触发分布式 行为------分别对每个成员求值再合并。与 infer 结合威力巨大:
ts
// 从一个联合类型中提取所有函数类型的参数
type ExtractFnArgs<T> = T extends (...args: infer A) => any ? A : never;
// 从多个函数类型中提取所有返回值
type ExtractResults<T> = T extends (...args: any[]) => infer R ? R : never;
// 实战:合并多个 handler 的类型
type Handlers = ((name: string) => void) | ((age: number) => boolean);
// in 拆分处理
type HandlerArgs = ExtractFnArgs<Handlers>;
// [string] | [number]
// ⚠️ 注意:是元组联合,不是 string | number
⚠️ 重要注意 :分布式条件类型只对「裸类型参数」生效。如果把 T 包在数组里,就不会分布式:
ts
// 不会分布式 ⚠️
type NonDistrib<T> = [T] extends [infer U] ? U : never;
要点总结
| 模式 | infer 位置 | 典型用途 |
|---|---|---|
| 函数提取 | (...args: infer A) => infer R |
提取参数、返回值类型 |
| Promise 拆包 | Promise<infer U> |
递归拆多层 Promise |
| 数组提取 | (infer U)[] / [infer F, ...] |
提取元素、首/尾类型 |
| 模板字面量 | ${string}:${infer P} |
路由参数解析 |
| 分布式 infer | 裸类型 + infer | 联合类型批量处理 |
三个容易踩的坑:
- infer 只能出现在 extends 的条件里 ------不能在
type X = infer T这种地方用 - 多 infer 变量是「并发」匹配的------同一个位置的 infer 变量名不同不会冲突
- 重载函数只匹配最后一个签名------如果需要精确提取,考虑改用接口
一句话总结 :infer 不是「声明类型」,而是 「让 TS 帮我推导出这个位置是什么类型」 ------理解了这个思维转换,你就能写出像 ReturnType、Awaited 这样的内置工具类型,还能自己定制企业级类型工具。