文章

infer关键字深入深度解析

深入解析TypeScript infer关键字的函数返回值类型提取、Promise包裹类型提取及在条件类型中的高级用法

infer关键字深入深度解析

一句话概括

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 时执行以下步骤:

  1. 解构检查:将 T 的结构与条件左侧的泛型模式逐一对照
  2. 变量声明infer X 在对照过程中声明一个未绑定类型变量
  3. 推导填充:TypeScript 搜索所有能让条件成立的 X 类型赋值
  4. 结果注入:在 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] 再次包裹

总结与扩展

核心要点

  1. infer 是条件类型的”局部变量”——声明在 extends 右侧,作用域仅限当前条件分支
  2. infer 的位置 = 想要提取的类型的位置:放在返回值位置就提取返回,放在参数位置就提取参数
  3. 递归 infer 是处理嵌套结构的利器:Promise 解包、深度只读都靠递归实现
  4. 联合类型会触发分发:用 [T] extends [infer U] 包裹泛型可阻止分发
  5. infer 不能独立存在:必须有 extends <pattern> 做模式匹配,它才能工作

延伸学习方向

  • infer + 映射类型:实现 DeepPick<T, K> 等深层属性选择工具
  • Variadic Tuple Typesinfer 配合展开运算符做复杂元组操作
  • infer 在声明文件(.d.ts)中的应用:理解内置工具类型如何实现
  • 递归条件类型的性能边界:了解 TypeScript 的深度限制(~50层)

相关主题

本文由作者按照 CC BY 4.0 进行授权