🧠 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 帮我推导出这个位置是什么类型」 ------理解了这个思维转换,你就能写出像 ReturnType、Awaited 这样的内置工具类型,还能自己定制企业级类型工具。

相关推荐
特立独行的猫A6 分钟前
我为什么推荐用仓颉语言的cj-tauri 写桌面应用(附 cjnote 最小的markdown笔记AI软件)
前端
纸片人10 分钟前
牛一越界,大屏 3 秒飞到它身边:我用真实卫星数据 + Three.js 做了个 3D 智慧牧场
前端
cc_瀚海知行20 分钟前
工具调用 6|助手什么时候该停下来?
java·服务器·前端·人工智能·python·ai
溪语流沙35 分钟前
【Web全栈进阶】Nginx反向代理 + HTTPS:正式上线
前端·vue.js·python·nginx·https·fastapi
vivo互联网技术1 小时前
域名埋点零侵入实时校验实践:基于代理转发与规则驱动的工具设计
前端·测试·监控·代理·埋点
喵个咪1 小时前
GoWind ShortPlay 拆解:Flutter 沉浸式 Feed 的播放器工程——生命周期、手势状态机与游客降级
前端·flutter·架构
白日做梦Q1 小时前
Chrome商店访问不了,无法现在安装插件?利用Edge商店迁移扩展完整实操教程问题背景
前端·chrome·edge
涛涛ing1 小时前
2026年,你的前端终于不用再“算随机数”和“量高度”了
前端
用户298698530141 小时前
React 前端 Word 转图片:页面与元素级导出的工程实践
前端·javascript·react.js