这节在地图里是高级类型的入口。条件类型让你写 T extends U ? X : Y,等价于类型层面的 if。infer 则像解构赋值,从复杂类型里"抠"出某一部分。两者合起来,能在编译期做相当复杂的推导。
type IsString<T> = T extends string ? true : false; type A = IsString<"hi">; // true type B = IsString<42>; // false
输入输出:传入字面量 "hi" 得 true,传入 42 得 false。这完全在编译期求值,运行时无痕迹。条件类型本身不产出代码,只产出"另一个类型"。
infer 声明一个"待推断的占位类型",用在条件类型的真分支里提取子类型。
// 提取函数返回值类型 type ReturnOf<F> = F extends (...args: any[]) => infer R ? R : never; type R1 = ReturnOf<() => number>; // number type R2 = ReturnOf<(x: string) => boolean>; // boolean // 提取数组元素类型 type ElementOf<A> = A extends (infer E)[] ? E : never; type E1 = ElementOf<string[]>; // string type E2 = ElementOf<number[]>; // number
背景:想拿到"某函数返回什么""某数组装什么",但不想手写重复声明。操作:用 infer R/infer E 占位。结果:ReturnOf/ElementOf 自动跟随传入类型。解读:infer 只能出现在 extends 条件里,它是编译器推断的钩子。变式:可同时 infer 多个位置,如提取 Promise 内部类型 Awaited(见下)。
// 递归提取 Promise 嵌套 type Awaited<T> = T extends Promise<infer U> ? Awaited<U> : T; type Deep = Awaited<Promise<Promise<number>>>; // number
当 T 是联合且出现在 T extends U 裸用(未被数组/元组包裹)时,会逐成员分发。
type ToArray<T> = T extends any ? T[] : never; type R = ToArray<string | number>; // string[] | number[],而非 (string|number)[]
输入输出:传入 string | number,结果是 string[] | number[](每个成员各自变数组),不是统一包一层。这是最容易算错的行为,记住"裸联合 + 条件 = 分发"。若想关掉分发,用元组包住:[T] extends [any]。
这张图把 infer 抽取过程可视化:

type ApiResult<T> = T extends Error ? { ok: false; error: string } : { ok: true; data: T }; function wrap<T>(v: T): ApiResult<T> { if (v instanceof Error) return { ok: false, error: v.message } as any; return { ok: true, data: v } as any; } const r1 = wrap(123); // { ok:true; data:number } const r2 = wrap(new Error("x")); // { ok:false; error:string }
infer 不只作用于返回值,参数列表、构造函数也能抽。
// 提取函数第一个参数类型 type FirstArg<F> = F extends (first: infer A, ...rest: any[]) => any ? A : never; type FA = FirstArg<(id: number, name: string) => void>; // number // 提取构造函数产出的实例类型 type InstanceOf<C> = C extends new (...args: any[]) => infer I ? I : never; class Point { x = 0; } type P = InstanceOf<typeof Point>; // Point
背景:写高阶函数、工厂装饰器时,常要"拿函数/类的某个位置类型"。操作:用 infer A/infer I 在对应位置占位。结果:FirstArg/InstanceOf 自动跟随。解读:new (...args) => infer I 是构造签名的专用形态,记住 new 关键字前缀即可抽取实例。
infer 能配合剩余元素抽取"前缀 / 后缀",这是实现 Concat、Tail 等工具的基础。
// 取元组除第一项外的尾部 type Tail<T extends any[]> = T extends [infer _, ...infer Rest] ? Rest : never; type T1 = Tail<[1, 2, 3]>; // [2, 3] // 反转式:取第一项 type Head<T extends any[]> = T extends [infer H, ...any[]] ? H : never; type H1 = Head<[1, 2, 3]>; // 1 // 在参数列表上同理(函数参数元组化) type DropFirst<F> = F extends (a: any, ...rest: infer R) => infer Ret ? (...args: R) => Ret : never; type D = DropFirst<(a: string, b: number) => boolean>; // (b: number) => boolean
背景:函数组合、中间件签名常要"去掉或变换首参"。操作:把元组剩余元素 ...infer Rest 抽到变量。结果:得到裁剪后的类型。解读:元组剩余推断是类型编程的高频技巧,5.2 映射类型会和它配合做键值变换。
条件类型可递归调用自己(如上面 Awaited),但必须有"基例"否则无限展开。基例通常是"不再满足 extends 条件"的分支返回某具体类型。
// 深度只读:对所有对象属性递归加 readonly type DeepReadonly<T> = { readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K]; }; // 基例:T[K] 不是 object 时直接返回,停止递归 interface Config { a: number; b: { c: string }; } type R = DeepReadonly<Config>; // R.b.c 也变成 readonly,递归在这里生效
背景:想让嵌套对象整体不可变。操作:在映射里对 object 类型递归调用自身。结果:每一层都被 readonly 覆盖。解读:递归条件类型的"安全"靠 extends 判断自然收口——值类型不再满足 extends object 就停,不会栈溢出。
条件类型 + infer 很香,但嵌套超过两层,同事就难读了。我们的纪律:工具类型(被反复用)可以复杂,业务代码里尽量用现成工具类型而非现场写复杂条件。把"聪明"封在 utils/types.ts 里,业务文件保持平实。
infer 不仅能抽返回值、数组元素,还能在元组里用剩余元素占位抽出"中段"。
// 去掉元组的首尾,保留中间 type Middle<T> = T extends [infer _F, ...infer M, infer _L] ? M : never; type R = Middle<[string, number, boolean, Date]>; // [number, boolean]
输入输出:传入四元元组,返回掐头去尾后的二元元组。下划线前缀 _F/_L 表示"占位但不使用",是 TS 允许的命名约定。解读:元组里的 infer 与剩余元素 ... 组合,能做相当精确的片段提取,是函数参数裁剪的基础。
条件类型配合 never 能递归地过滤出"满足某条件的键",衍生出内置 Extract 的进阶版。
// 只保留"值可赋给 string"的键 type StringKeys<T> = { [K in keyof T]: T[K] extends string ? K : never; }[keyof T]; interface Row { id: number; name: string; code: string; flag: boolean } type S = StringKeys<Row>; // "name" | "code"
背景:想批量拿到"某类值的字段名",用于生成表单字段列表或校验白名单。操作:用映射给每个键打上"键名或 never",再索引 keyof T 把 never 吸收。结果:S 精确是那两个字符串字段。解读:这种"映射 + 索引触发分布"的套路在高级类型里极常用,记住它能省很多手写联合。
T extends U ? X : Y 是类型层 ifinfer 在条件真分支抽取子类型,只能用于 extends⚠️ 别在业务代码里堆三层以上条件类型。它难读难改,新人接手会绕晕;把复杂逻辑收进类型工具库,业务侧只调用结果。
💡 写 infer 提取时,先在注释里用 JS 伪代码写出"我要抠哪一段",再把那段换成 infer X,比凭空硬写命中率高很多。