条件类型与infer深度解析
条件类型 T extends U 问号 X 否则 Y 是 TS 类型系统的 if/else,infer 则是其中的模式匹配解构。 两者组合能从函数签名、Promise、数组中精确提取内部类型,ReturnType 与 Parameters 就是它们最著名的产品,面试手写必考。
一句话概括
条件类型 T extends U ? X : Y 是 TypeScript 类型系统的 if/else,而 infer 是其中的”模式匹配解构”——两者组合让你能从函数签名、Promise、数组等复杂类型中精确提取内部信息,ReturnType<T> 和 Parameters<T> 就是它们最著名的产品。
核心知识点
1. 分布式条件类型:联合类型自动”map”
这是最常见的面试坑——当条件类型作用于裸泛型参数时,会把联合类型拆开逐条判断,再把结果联合起来:
1
2
3
4
5
6
7
8
type IsString<T> = T extends string ? 'yes' : 'no';
type A = IsString<'a' | 1>; // 'yes' | 'no' ← 分布式:先 IsString<'a'>→'yes',再 IsString<1>→'no',结果取并集
type B = IsString<'a' | 'b'>; // 'yes' ← 拆开都是 'yes',并集等于自己
// ⚠️ 阻止分布式:用元组包裹
type NotDistributed<T> = [T] extends [string] ? 'yes' : 'no';
type C = NotDistributed<'a' | 1>; // 'no' ← 整个 ('a'|1) 去匹配 [string],不成立
核心结论:联合类型 + 裸泛型 + extends = 分布式计算。Exclude<T, U> 正是靠这个特性工作的——T extends U ? never : T 对联合类型 T 的每个成员执行过滤。
2. infer:在条件类型中做”模式匹配”
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 最简示例:提取数组元素类型
type ArrayItem<T> = T extends (infer U)[] ? U : never;
type X = ArrayItem<string[]>; // string
type Y = ArrayItem<number>; // never ← 不匹配,走 else 分支
// 提取函数返回值(官方 ReturnType 的实现)
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
// 提取函数参数
type MyParameters<T> = T extends (...args: infer P) => any ? P : never;
// 提取 Promise 包裹的值(Awaited 的前身)
type Unwrap<T> = T extends Promise<infer V> ? V : T;
type U = Unwrap<Promise<string>>; // string
type V = Unwrap<Promise<Promise<number>>>; // Promise<number> ← 只解一层,要用递归
infer 只能用在条件类型的 extends 后面,且只能在 true 分支中使用。一个条件类型中可以有多个 infer:
1
2
3
// 同时提取参数和返回值
type FnShape<T> = T extends (...args: infer P) => infer R ? { params: P; return: R } : never;
type S = FnShape<(x: number, y: string) => boolean>; // { params: [number, string]; return: boolean }
3. 协变与逆变位置中的 infer
这是 TypeScript 面试的高级话题——同一个 infer 在不同位置有不同的推断行为:
1
2
3
4
5
6
7
8
// 协变位置(返回值):推断为联合类型
type Covariant<T> = T extends { a: infer U; b: infer U } ? U : never;
type C = Covariant<{ a: string; b: number }>; // string | number ← 联合
// 逆变位置(参数):推断为交叉类型
type Contravariant<T> = T extends { fn: (x: infer U) => void; gn: (x: infer U) => void } ? U : never;
type D = Contravariant<{ fn: (x: string) => void; gn: (x: number) => void }>; // string & number ← 交叉
// 因为函数参数是逆变的——一个同时能处理 string 和 number 的参数,必须是 string & number(never)
这在写 UnionToIntersection 时至关重要,下面会有实际案例。
4. 分布式 + infer 的高级组合:UnionToIntersection
1
2
3
4
5
// 将联合类型转为交叉类型
type UnionToIntersection<U> =
(U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never;
type Result = UnionToIntersection<{ a: 1 } | { b: 2 }>; // { a: 1 } & { b: 2 }
原理分两步:
- 分布式:
U extends any ? (k: U) => void : never把{a:1}|{b:2}变成((k: {a:1}) => void) | ((k: {b:2}) => void) - 函数参数逆变:
((k:{a:1})=>void) | ((k:{b:2})=>void)匹配(k: infer I) => void时,由于函数参数位置是逆变的,I 推断为两个参数的交叉:{a:1} & {b:2}
5. 条件类型 + never 的”短路”特性
1
2
3
4
5
6
7
8
// never 在条件类型中的特殊行为
type IsNever<T> = T extends never ? true : false;
type Test = IsNever<never>; // never ← 不是 true!
// 原因:never 是空联合类型,分布式条件类型对 never 直接短路返回 never
// 正确写法:包装一下防止分布式
type IsNeverFixed<T> = [T] extends [never] ? true : false;
type Test2 = IsNeverFixed<never>; // true ✅
其实你每天都在用
- React 的
useMemo/useCallback类型推导——这两个 Hook 的泛型参数就是通过infer从传入的回调函数中提取参数和返回值的,保证类型安全而不需要手动标注。 Promise.then的类型链——.then(res => ...)中的res类型是前一个 Promise 解包的结果。TypeScript 通过条件类型T extends Promise<infer V>实现这个链式推导——你写的fetch().then(r => r.json()).then(data => ...)每一步类型都自动推导,背后全是infer。- Axios 响应类型——
axios.get<User>('/api/user')返回AxiosResponse<User>,其中data字段能从泛型User自动推导。拦截器的类型也依赖条件类型来区分请求/响应拦截。 - Vue3
defineProps+defineEmits类型提取——defineEmits<{ change: [id: number] }>()的emit('change', 1)类型检查,底层是Parameters模式:从函数签名中提取参数元组。 - Prisma / tRPC 的端到端类型安全——tRPC 用条件类型 + infer 从后端路由定义中自动提取前端调用类型,你写
trpc.user.list.useQuery()时 IDE 知道返回的data是什么结构,全链路零手动标注。
常见误解(FAQ)
❌ 误区:「条件类型的
extends和 JS 的extends一样」 真相:TS 的extends在条件类型中是类型兼容性检查,'a' extends string为 true,string extends 'a'为 false——它判断的是”左边是否是右边的子类型”,不是”左边是否继承自右边”。1 | 2 extends number是 true,number extends 1 | 2是 false。❌ 误区:「
infer能在任何位置使用」 真相:infer只能出现在条件类型的extends子句中(而且只能在true分支中引用)。type X = infer T会直接报错。它的语义是”在此处声明一个待推断的类型变量”。❌ 误区:「分布式条件类型永远是好事」 真相:大多数时候它确实符合直觉,但当你不想分布式时就是 bug 来源。例如写
IsNever<T>时IsNever<never>返回never而非true,就是因为never是空联合,分布式直接走空了。用[T] extends [never]包装即可避免。❌ 误区:「
ReturnType<typeof fn>总能拿到正确的返回值类型」 真相:对重载函数,ReturnType只取最后一个重载签名的返回值。对泛型函数,如果没有具体类型参数传入,ReturnType推断出的类型可能不精确。这也是为什么 React 的useCallback不依赖ReturnType而是用Parameters模式。
一句话总结
T extends U ? X : Y 给了 TypeScript 类型系统做决策的能力,infer 给了它拆解结构的能力——这两个特性组合起来,让类型系统从”静态标注”进化到了”静态计算”,你写的每一行类型都能像运行时代码一样执行逻辑、提取信息、递归推导。