🧠 TypeScript 条件类型 + infer:从「类型体操」到生产实战的 5 个模式

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 联合类型批量处理

三个容易踩的坑:

  1. infer 只能出现在 extends 的条件里 ------不能在 type X = infer T 这种地方用
  2. 多 infer 变量是「并发」匹配的------同一个位置的 infer 变量名不同不会冲突
  3. 重载函数只匹配最后一个签名------如果需要精确提取,考虑改用接口

一句话总结infer 不是「声明类型」,而是 「让 TS 帮我推导出这个位置是什么类型」 ------理解了这个思维转换,你就能写出像 ReturnTypeAwaited 这样的内置工具类型,还能自己定制企业级类型工具。

相关推荐
IT_陈寒15 小时前
Vite静态资源路径这个坑差点让我加班到凌晨
前端·人工智能·后端
掘金酱15 小时前
「TRAE Work 实战帮」征文启动!你沉淀的经验,值得被看见!
前端·人工智能·后端
橙子家15 小时前
Windows 上同时安装多个 node 版本
前端
晓说前端15 小时前
TypeScript 高级特性 —— 类型断言与泛型
前端·typescript
爱勇宝16 小时前
DeepSeek V4-Flash 更新:代码与 Agent 能力全面增强
前端·后端·deepseek
GuWenyue16 小时前
90%前端写React+TS都踩坑!从组件类型、单向数据流到本地存储完整实战
前端·react.js
妙码生花16 小时前
从 PHP 到 AI + Golang,程序员自救转型手记(四十五):前端远程下拉输入组件
前端·javascript·vue.js
明月_清风16 小时前
全栈工程师必会技术栈:从入门到架构的完整成长地图 🗺️
前端·后端·全栈
windliang16 小时前
Claude Code 源码分析(三):一次模型回答如何流进 Agent
前端·算法·ai编程