infer关键字深入深度解析
深入解析TypeScript infer关键字的函数返回值类型提取、Promise包裹类型提取及在条件类型中的高级用法
一句话概括
infer 是 TypeScript 条件类型中的”类型变量”,让你在类型层级做模式匹配时实时”捕获”并命名未知部分,从而提取、解构、重构任意复杂类型。
背景
假设你拿到一个函数类型,想提取它的返回值类型,该怎么做?TypeScript 没有 func.returnType 这种语法。你需要的是一种在类型层面”问问题”并”记住答案”的机制——infer 就是这个机制。
1
2
3
4
5
6
7
8
9
10
11
12
// 没有 infer,想拿到返回值类型只能靠声明冗余类型
function fetchUser(): Promise<{ name: string; age: number }> { /* ... */ }
// 想声明一个变量类型与 fetchUser 的返回值匹配,必须手动写一遍
const result: Promise<{ name: string; age: number }> = fetchUser()
// ✗ 重复劳动,危险——如果函数签名改了,result 类型不会报错
// 有 infer,直接提取
type FetchResult = Awaited<ReturnType<typeof fetchUser>> // string ✗
// 正确:
type FetchResult = Awaited<ReturnType<typeof fetchUser>>
// FetchResult === { name: string; age: number }
infer 是面试高频考点,它贯穿了 TypeScript 类型工具设计的核心哲学——类型即代码,代码即类型。本文的知识点恰好衔接昨天学习的”类型守卫”,并为明天”手写内置类型工具”打下基础。
概念与定义
什么是 infer?
infer 是 TypeScript 2.8 引入的关键字,只能出现在条件类型(Conditional Type)的 extends 子句中。它的作用是”在这个位置声明一个局部类型变量,TypeScript 会在匹配时自动推断它的具体类型”。
1
2
3
4
5
6
7
// 语法结构
type Extract<Type, Condition> = Type extends Condition ? Type : never
// infer 的标准形式:在 extends 条件中声明一个类型变量 T
type MyInfer<T> = T extends infer U ? U : never
// ^^^^^
// 声明一个名为 U 的类型变量,TypeScript 会帮我们填充
核心规则:
infer只能在条件类型的extends右侧(true 分支)使用infer X声明的X只能在当前条件的 true 分支中引用- 每个条件类型可以有多个
infer,在同一层级可捕获多个位置
最小示例
1
2
3
4
5
6
7
8
9
// 最简单的 infer:从函数类型中提取返回值
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never
// 使用
function greet(name: string): string {
return `Hello, ${name}`
}
type GreetReturn = ReturnType<typeof greet>
// GreetReturn === string ✅
这一行代码展现了 infer 的全部哲学:在模式匹配中声明变量,在推断中获取类型。
核心知识点拆解
1. 提取函数返回值类型
infer 最经典的应用就是手写 ReturnType。
1
2
3
4
5
6
7
8
9
// 手写版(标准答案)
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never
// 多层函数(返回函数)
function compute(): () => number {
return () => 42
}
type ComputedType = MyReturnType<typeof compute>
// ComputedType === () => number ✅
2. 提取函数参数类型
用同样的模式,但 infer 的位置在参数列表中。
1
2
3
4
5
6
7
8
9
10
type MyParameters<T> = T extends (...args: infer P) => any ? P : never
function processUser(name: string, age: number, active: boolean) {}
type Params = MyParameters<typeof processUser>
// Params === [string, number, boolean] ✅
// 提取第一个参数
type FirstArg<T> = T extends (first: infer F, ...rest: any[]) => any ? F : never
type First = FirstArg<typeof processUser>
// First === string ✅
3. 提取数组元素类型
将数组看做一种结构化类型,用条件类型做模式匹配。
1
2
3
4
5
6
7
8
9
10
11
12
type ElementType<T> = T extends (infer E)[] ? E : never
type StrArr = ElementType<string[]>
// StrArr === string ✅
type NumArr = ElementType<number[]>
// NumArr === number ✅
// 提取元组中特定位置的元素
type Second<T extends any[]> = T extends [any, infer S, ...any[]] ? S : never
type B = Second<[string, number, boolean]>
// B === number ✅
4. 提取 Promise 包裹类型(含递归解包)
这是 infer 最硬核的应用之一——处理异步类型。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 单层提取
type Awaited<T> = T extends Promise<infer V> ? V : T
type Str = Awaited<Promise<string>>
// Str === string ✅
// 递归版:解开任意层级的 Promise
type DeepAwaited<T> =
T extends Promise<infer V>
? DeepAwaited<V> // 递归解包
: T
type TripleNested = DeepAwaited<Promise<Promise<Promise<{ id: number }>>>>
// TripleNested === { id: number } ✅
5. 提取构造函数实例类型
对类使用 infer 可以捕获构造后的实例类型。
1
2
3
4
5
6
7
8
9
10
11
12
type InstanceType<T> = T extends new (...args: any[]) => infer I ? I : never
class User {
constructor(public name: string, public age: number) {}
}
type UserInstance = InstanceType<typeof User>
// UserInstance === User ✅
// 提取构造函数参数
type ConstructorParams<T> = T extends new (...args: infer P) => any ? P : never
type UserCtorParams = ConstructorParams<typeof User>
// UserCtorParams === [string, number] ✅
6. infer 的约束与多位置推断
infer 可以带约束(但不是 TypeScript 语法上的约束),通过条件类型的多重匹配实现复杂场景。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 多 infer 在同一条条件中:同时提取 key 和 value
type ExtractKeyValue<T> = T extends { key: infer K; value: infer V } ? [K, V] : never
type KV = ExtractKeyValue<{ key: 'username'; value: string }>
// KV === ['username', string] ✅
// 带约束的 infer:要求被推断的类型满足某种结构
type NonNullable<T> = T extends null | undefined ? never : T
// 配合 infer 提取非 null/undefined 的值
type Definite<T> = T extends infer U ? Exclude<U, null | undefined> : never
// infer 在联合类型中的分发行为
type ToArray<T> = T extends infer U ? U[] : never
type Result = ToArray<string | number>
// Result === string[] | number[](联合类型中的每个成员分别匹配)
7. infer 在模板字面量类型中的使用
TS4.1 之后,infer 可以配合模板字面量做字符串操作。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 提取字符串字面量类型的某一部分
type ExtractRoute<T extends string> = T extends `/api/${infer Path}` ? Path : never
type Users = ExtractRoute<'/api/users'>
// Users === 'users' ✅
// 提取域名部分
type ExtractHost<T extends string> = T extends `https://${infer Host}/path` ? Host : never
type Domain = ExtractHost<'https://api.example.com/path'>
// Domain === 'api.example.com' ✅
// 联合类型的模式匹配
type ExtractPaths<T extends string> = T extends `/api/${infer P}` ? P : never
type AllPaths = ExtractPaths<'/api/users' | '/api/posts' | '/api/comments'>
// AllPaths === 'users' | 'posts' | 'comments' ✅
实战案例
案例一:类型安全的 API 响应提取器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 定义 API 响应结构
interface ApiResponse<T> {
code: number
data: T
message: string
}
// 提取 data 的类型(忽略包装层)
type ApiData<T> = T extends { data: infer D } ? D : never
// 使用
async function fetchUsers(): Promise<ApiResponse<{ id: number; name: string }[]>> {
return { code: 200, data: [{ id: 1, name: 'Alice' }], message: 'success' }
}
type UserList = Awaited<ReturnType<typeof fetchUsers>>
type ActualData = ApiData<UserList>
// ActualData === { id: number; name: string }[] ✅
案例二:事件处理函数类型提取
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 提取事件对象的 eventType
type EventHandlerResult<T> = T extends (event: infer E) => void ? E : never
function handleClick(event: MouseEvent & { target: HTMLElement }) {
console.log(event.target.tagName)
}
function handleKeydown(event: KeyboardEvent) {
console.log(event.key)
}
// 提取处理器对应的参数类型
type ClickEvent = EventHandlerResult<typeof handleClick>
// ClickEvent === MouseEvent & { target: HTMLElement } ✅
type KeyEvent = EventHandlerResult<typeof handleKeydown>
// KeyEvent === KeyboardEvent ✅
案例三:实现递归类型工具
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// DeepReadonly:递归地将所有嵌套属性设为 readonly
type DeepReadonly<T> = T extends object
? { readonly [K in keyof T]: DeepReadonly<T[K]> }
: T
interface Config {
db: {
host: string
port: number
credentials: {
user: string
password: string
}
}
}
type ReadonlyConfig = DeepReadonly<Config>
// 所有层级的属性都变成 readonly ✅
// DeepPartial:递归地让所有属性可选
type DeepPartial<T> = T extends object
? { [K in keyof T]?: DeepPartial<T[K]> }
: T
type PartialConfig = DeepPartial<Config>
// 所有层级的属性都变成可选 ✅
// DeepPromise:将所有嵌套值包装为 Promise
type DeepPromise<T> = T extends object
? { [K in keyof T]: DeepPromise<T[K]> }
: Promise<T>
type PromisedConfig = DeepPromise<Config>
// 嵌套值都变成 Promise 类型 ✅
底层原理
模式匹配算法
TypeScript 编译 T extends infer U ? X : Y 时执行以下步骤:
- 解构检查:将
T的结构与条件左侧的泛型模式逐一对照 - 变量声明:
infer X在对照过程中声明一个未绑定类型变量 - 推导填充:TypeScript 搜索所有能让条件成立的
X类型赋值 - 结果注入:在 true 分支中,所有
X引用被替换为推导出的具体类型
1
2
3
4
5
6
7
8
9
// 演示推断过程
type Demo<T> = T extends (a: infer A, b: infer B) => infer R ? [A, B, R] : never
// 分解 T = (a: string, b: number) => boolean
// 对照:(a: string, b: number) => boolean
// (a: infer A, b: infer B ) => infer R
// 结果:A = string, B = number, R = boolean
type Result = Demo<(a: string, b: number) => boolean>
// Result === [string, number, boolean]
infer 的作用域规则
infer声明的类型变量仅在当前条件分支(?和:之间)的表达式中有效- 嵌套的条件类型中,内层
infer可以覆盖外层同名变量(就近原则) - 同一层级多个
infer的关系是并行匹配,而非串行依赖
💡 人话总结:把 infer 想象成”类型世界的正则捕获组”。条件类型定义了匹配规则,infer 负责把匹配到的片段捞出来、给它起个名字、交给后续代码使用。它不创造类型,只是发现并命名已有结构中的隐藏部分。
高频面试题解析
Q1:infer 只能在哪里使用?它的作用是什么?
答:infer 只能出现在条件类型的 extends 子句右侧(即 ? 左边),它的作用是在类型模式匹配过程中声明一个局部类型变量,使 TypeScript 自动推断并填充该位置的具体类型。它的核心价值在于在类型层面做运行时才能做到的事情——从未知类型中提取已知片段。
Q2:如何提取嵌套 Promise 的内部类型?
答:使用递归条件类型 + infer。
1
2
3
4
5
6
type DeepUnwrapPromise<T> = T extends Promise<infer V>
? DeepUnwrapPromise<V> // 递归解包,直到不再是 Promise
: T // 基础情况:返回原类型
type T1 = DeepUnwrapPromise<Promise<Promise<number>>> // number
type T2 = DeepUnwrapPromise<Promise<{ name: string }>> // { name: string }
Q3:手写 ReturnType 和 Parameters
答:
1
2
3
4
5
6
7
8
9
10
11
12
13
// ReturnType:提取函数返回值
type MyReturnType<T extends (...args: any[]) => any> =
T extends (...args: any[]) => infer R ? R : never
// Parameters:提取函数参数为元组
type MyParameters<T extends (...args: any[]) => any> =
T extends (...args: infer P) => any ? P : never
// 验证
function demo(a: string, b: number, c: boolean) { return 'done' }
type R = MyReturnType<typeof demo> // string
type P = MyParameters<typeof demo> // [string, number, boolean]
Q4:infer 在什么情况下会产生联合类型?如何避免?
答:当条件类型的泛型是联合类型时,TypeScript 会分发条件类型,每个联合成员独立匹配,产生联合的 infer 结果。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
type ToArray<T> = T extends infer U ? [U] : never
// 分发:string extends ... + number extends ... + boolean extends ...
type R = ToArray<string | number | boolean>
// R === [string] | [number] | [boolean] (联合类型)
// 避免方法一:用元组包裹泛型,阻止分发
type ToArrayFixed<T> = [T] extends [infer U] ? [U] : never
type R2 = ToArrayFixed<string | number | boolean>
// R2 === [string | number | boolean](单个元组,联合作为整体)
// 避免方法二:在分发后用交叉类型合并
type MergeArray<T> = T extends infer U ? (U | never) : never
// 或用 [U] 再次包裹
总结与扩展
核心要点
infer是条件类型的”局部变量”——声明在extends右侧,作用域仅限当前条件分支- infer 的位置 = 想要提取的类型的位置:放在返回值位置就提取返回,放在参数位置就提取参数
- 递归 infer 是处理嵌套结构的利器:Promise 解包、深度只读都靠递归实现
- 联合类型会触发分发:用
[T] extends [infer U]包裹泛型可阻止分发 infer不能独立存在:必须有extends <pattern>做模式匹配,它才能工作
延伸学习方向
infer+ 映射类型:实现DeepPick<T, K>等深层属性选择工具- Variadic Tuple Types:
infer配合展开运算符做复杂元组操作 infer在声明文件(.d.ts)中的应用:理解内置工具类型如何实现- 递归条件类型的性能边界:了解 TypeScript 的深度限制(~50层)
相关主题
- 📘 类型守卫的实现方式(前一天:类型守卫是类型收窄的基础,infer 是类型提取的基础)
- 📘 手写 Partial/Required/Pick/Omit(第二天:使用映射类型+条件类型+infer 完整手写所有内置工具类型)
- 📘 模板字面量类型与字符串操作(infer 在模板字面量中的高级用法)