条件类型与infer深度解析
一句话概括
条件类型(T extends U ? X : Y)是 TypeScript 类型系统的”if/else”,而 infer 是其中的”模式匹配”工具——两者组合让类型系统具备了从复杂函数签名中提取信息的能力,ReturnType<T> 和 Parameters<T> 就是最经典的产物。
一、背景与意义
1.1 为什么需要条件类型?
在没有条件类型之前,TypeScript 的类型系统虽然强大,但缺少分支能力。举个例子:
1
2
3
4
5
6
// 需求:如果 T 是 string,返回 string;如果是 number/boolean,返回它们的字符串表示
// 没有条件类型时,我们只能这样:
type ToString<T> = string; // ❌ 无法区分输入类型
// 有了条件类型:
type ToString<T> = T extends string ? T : T extends number ? `${T}` : string;
条件类型使类型系统真正有了 “编程能力”——可以根据输入类型的不同,产生不同的输出类型。
1.2 条件类型的主要应用场景
- 类型过滤:
Exclude<T, U>、Extract<T, U>从联合类型中筛选或排除 - 类型提取:
ReturnType<T>、Parameters<T>从函数类型中提取信息 - 类型映射:将一种类型变换为另一种(如
Promise<string>→string) - 类型验证:检查类型是否符合某种约束
- 递归类型处理:配合递归实现深层类型变换
1.3 infer 关键字的诞生
infer 引入于 TypeScript 2.8(与条件类型一同发布),其灵感来源于函数式编程的模式匹配(Pattern Matching):
1
2
3
4
5
6
7
8
// Haskell 中的模式匹配
-- 类型级别的模式匹配
length :: [a] -> Int
length [] = 0
length (_:xs) = 1 + length xs
// TypeScript 中的 infer
type ExtractArrayType<T> = T extends (infer U)[] ? U : never;
二、概念与定义
2.1 条件类型基础语法
1
2
3
4
5
6
7
// 基本语法
type Conditional<T> = T extends U ? X : Y;
// 示例
type IsString<T> = T extends string ? 'yes' : 'no';
type Test1 = IsString<'hello'>; // 'yes'
type Test2 = IsString<42>; // 'no'
2.2 分布式条件类型(Distributive Conditional Types)
当条件类型作用于泛型参数且该参数是裸类型参数时,条件类型会自动分布到联合类型的每个成员:
1
2
3
4
5
6
7
8
// 裸类型参数——条件类型会分布式执行
type ToArray<T> = T extends any ? T[] : never;
type Test = ToArray<string | number>; // string[] | number[](分布了)
// 而不是 (string | number)[]
// 包装后的类型参数——不会分布式执行
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;
type Test2 = ToArrayNonDist<string | number>; // (string | number)[]
2.3 infer 语法
infer 只能在条件类型的 extends 子句中使用,用于声明一个类型变量,被推断出的类型会绑定到这个变量上:
1
2
3
4
5
6
7
8
9
10
11
// 基本语法:T extends SomeType<infer U> ? U : never;
// 提取数组元素类型
type ElementType<T> = T extends (infer U)[] ? U : never;
type E1 = ElementType<string[]>; // string
type E2 = ElementType<number[]>; // number
// 提取 Promise 泛型参数
type Unwrap<T> = T extends Promise<infer U> ? U : T;
type U1 = Unwrap<Promise<string>>; // string
type U2 = Unwrap<number>; // number(不匹配,返回 T 自身)
三、最小示例:手写 Exclude、Extract、ReturnType、Parameters
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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
// ========== 1. Exclude<T, U> ==========
// 从联合类型 T 中排除可以赋值给 U 的成员
type MyExclude<T, U> = T extends U ? never : T;
// 验证
type T1 = MyExclude<'a' | 'b' | 'c', 'a' | 'b'>;
// 计算过程(分布式条件类型):
// 'a' extends 'a' | 'b' → yes → never
// 'b' extends 'a' | 'b' → yes → never
// 'c' extends 'a' | 'b' → no → 'c'
// 结果:'c'
type T2 = MyExclude<string | number | boolean, string | boolean>;
// string → never, number → number, boolean → never
// 结果:number
// ========== 2. Extract<T, U> ==========
// 从联合类型 T 中提取可以赋值给 U 的成员
type MyExtract<T, U> = T extends U ? T : never;
// 验证
type T3 = MyExtract<'a' | 'b' | 'c', 'a' | 'c'>;
// 'a' → 'a', 'b' → never, 'c' → 'c'
// 结果:'a' | 'c'
// ========== 3. ReturnType<T> ==========
// 提取函数类型的返回值类型
type MyReturnType<T extends (...args: any) => any> =
T extends (...args: any) => infer R ? R : any;
// 验证
type Func1 = () => string;
type R1 = MyReturnType<Func1>; // string
type Func2 = (x: number, y: string) => Promise<boolean>;
type R2 = MyReturnType<Func2>; // Promise<boolean>
// 泛型函数
function identity<T>(value: T): T { return value; }
type IdentityType = typeof identity;
type R3 = MyReturnType<IdentityType>; // unknown(如果没指定 T)
// ========== 4. Parameters<T> ==========
// 提取函数类型的参数列表类型(元组类型)
type MyParameters<T extends (...args: any) => any> =
T extends (...args: infer P) => any ? P : never;
// 验证
type Func3 = (name: string, age: number, active: boolean) => void;
type P1 = MyParameters<Func3>; // [name: string, age: number, active: boolean]
// 空参数
type Func4 = () => void;
type P2 = MyParameters<Func4>; // []
// 搭配 ...rest
type Func5 = (...args: string[]) => void;
type P3 = MyParameters<Func5>; // string[]
// ========== 验证代码 ==========
function testUtilityTypes() {
// Exclude
const excluded: MyExclude<'a' | 'b' | 'c', 'a'> = 'b'; // OK
// const excluded2: MyExclude<'a' | 'b' | 'c', 'a'> = 'a'; // ❌ Error
// Extract
const extracted: MyExtract<'a' | 'b' | 'c', 'b' | 'c'> = 'b'; // OK
// const extracted2: MyExtract<'a' | 'b' | 'c', 'b' | 'c'> = 'a'; // ❌ Error
// ReturnType
function getDate(): Date { return new Date(); }
const dateFn: MyReturnType<typeof getDate> = new Date(); // OK
// const dateFn2: MyReturnType<typeof getDate> = 42; // ❌ Error
// Parameters
function greet(name: string, times: number): string {
return `${'Hi, '.repeat(times)}${name}`;
}
const params: MyParameters<typeof greet> = ['Alice', 3]; // OK
// const params2: MyParameters<typeof greet> = ['Alice']; // ❌ Error: 需要两个参数
console.log('All type checks passed!');
}
四、核心知识点拆解
4.1 分布式条件类型的陷阱
分布式条件类型(DCT)是条件类型最强大也最容易出错的特性。
陷阱 1:非裸类型参数不会分布
1
2
3
4
5
6
7
8
9
// 裸参数——会分布
type IsString<T> = T extends string ? true : false;
type Test1 = IsString<string | number>; // boolean(true | false)
// 计算:string→true, number→false → true | false = boolean
// 包装后的参数——不会分布
type IsStringNonDist<T> = [T] extends [string] ? true : false;
type Test2 = IsStringNonDist<string | number>; // false
// 计算:[string | number] extends [string] → false(整体比较)
陷阱 2:never 在 DCT 中的行为
1
2
3
type TestNever<T> = T extends string ? 'yes' : 'no';
type TN = TestNever<never>; // never
// 注意:never 被认为是空联合,DCT 不会对空联合产生任何成员
这与直觉不符——你可能期待 'yes' | 'no'。要解决这个问题,需要用 [T] extends [string] 包装:
1
2
type TestNeverFixed<T> = [T] extends [string] ? 'yes' : 'no';
type TNFixed = TestNeverFixed<never>; // 'yes' | 'no'
陷阱 3:复杂条件在 DCT 中的表现
1
2
3
type Test<T> = T extends { id: infer I } ? I : never;
type T1 = Test<{ id: number } | { id: string }>; // number | string
// 这是合理的——分布式执行后合并结果
4.2 infer 的位置和绑定
infer 可以出现在 extends 子句的任何位置,支持多种模式的匹配:
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
// 1. 函数参数中 infer
type FirstArg<T> = T extends (first: infer F, ...rest: any[]) => any ? F : never;
type FA = FirstArg<(name: string, age: number) => void>; // string
// 2. 函数返回值中 infer
type ReturnPromise<T> = T extends (...args: any[]) => Promise<infer R> ? R : never;
type RP = ReturnPromise<() => Promise<number>>; // number
// 3. 构造函数中 infer
type InstanceType<T> = T extends new (...args: any[]) => infer R ? R : any;
// 4. 数组/元组中 infer
type First<T extends any[]> = T extends [infer F, ...any[]] ? F : never;
type Second<T extends any[]> = T extends [any, infer S, ...any[]] ? S : never;
type Last<T extends any[]> = T extends [...any[], infer L] ? L : never;
// 5. 模板字符串中 infer(TS 4.1+)
type ExtractRouteParam<T extends string> =
T extends `${string}/:${infer Param}/${string}` ? Param : never;
type EP = ExtractRouteParam<'/users/:id/posts'>; // 'id'
// 6. 多层嵌套 infer
type DeepPromise<T> = T extends Promise<infer U>
? U extends Promise<infer V> ? V : U
: T;
type DP = DeepPromise<Promise<Promise<string>>>; // string
// 7. 链式 infer
type ChainUnwrap<T> = T extends Promise<infer U>
? ChainUnwrap<U>
: T;
type CU = ChainUnwrap<Promise<Promise<Promise<number>>>>; // number
4.3 逆变(Contravariance)与 Parameters
TypeScript 中的函数参数是逆变的,这影响了 infer 在参数位置的推断:
1
2
3
4
5
6
7
8
9
10
type Func<T> = (arg: T) => void;
// 正常情况下:Func<string | number> 可以赋值给 Func<string> 吗?
// (arg: string | number) => void 可以赋值给 (arg: string) => void 吗?
// 答:可以,因为 string | number 是 string 的超集
// infer 在参数位置
type ParamType<T> = T extends (arg: infer P) => any ? P : never;
type PT = ParamType<(arg: string | number) => void>;
// P 被推断为 string | number(参数位置的 infer 保留联合类型)
这与返回值位置不同——返回值是协变的:
1
2
3
type ReturnTypeInfer<T> = T extends (...args: any[]) => infer R ? R : never;
type RT = ReturnTypeInfer<() => string | number>;
// R 被推断为 string | number
4.4 多个 infer 占位符
同一条条件类型中可以有多个 infer:
1
2
3
4
5
6
7
8
9
10
// 提取函数参数和返回值(一次性提取)
type FuncInfo<T extends (...args: any) => any> =
T extends (...args: infer P) => infer R
? { params: P; returnType: R }
: never;
type FI = FuncInfo<(name: string, age: number) => boolean>;
// { params: [name: string, age: number]; returnType: boolean; }
// 提取 Promise 嵌套链(多级提取会复杂化,可用递归代替)
4.5 条件类型中的类型等价性
条件类型中的 extends 检查的是类型兼容性(assignability),不是类型等价性(equality):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 兼容性检查
type Check<T, U> = T extends U ? true : false;
type C1 = Check<{ name: string }, {}>; // true({name: string} 是 {} 的子类型)
type C2 = Check<1 | 2, 1>; // false(联合类型不是子类型)
// 如果需要严格的等价性检查,可以这样:
type IsEqual<T, U> =
[T] extends [U]
? [U] extends [T]
? true
: false
: false;
type E1 = IsEqual<string, string>; // true
type E2 = IsEqual<{ name: string }, {}>; // false(双向检查)
五、实战案例:类型安全的 API 客户端
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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
// ========== 场景:定义类型安全的 API 调用函数 ==========
// API 定义
interface APIDefinitions {
'/users': {
GET: {
params: { page?: number; limit?: number };
response: { users: Array<{ id: number; name: string }>; total: number };
};
POST: {
body: { name: string; email: string };
response: { id: number; name: string; email: string };
};
};
'/users/:id': {
GET: {
params: {};
response: { id: number; name: string; email: string; createdAt: string };
};
PUT: {
body: { name?: string; email?: string };
response: { id: number; name: string; email: string };
};
DELETE: {
params: {};
response: { success: boolean };
};
};
'/posts': {
GET: {
params: { authorId?: number; tag?: string };
response: Array<{ id: number; title: string; content: string; authorId: number }>;
};
};
}
// ---------- 工具类型 ----------
// 提取 API 方法类型
type ExtractMethod<T, Method extends string> =
T extends Record<string, any>
? T[Method extends keyof T ? Method : never]
: never;
// 提取路径参数(从路由模板中提取动态参数名)
type ExtractPathParams<Path extends string> =
Path extends `${string}:${infer Param}/${infer Rest}`
? { [K in Param | keyof ExtractPathParams<`/${Rest}`>]: string }
: Path extends `${string}:${infer Param}`
? { [K in Param]: string }
: {};
// 提取 API 方法的请求参数类型
type RequestParams<
Definitions,
Path extends keyof Definitions,
Method extends keyof Definitions[Path]
> = Definitions[Path][Method] extends { body: infer B }
? Definitions[Path][Method] extends { params: infer P }
? B & P
: B
: Definitions[Path][Method] extends { params: infer P }
? P
: {};
// 提取 API 方法的返回类型
type ResponseType<
Definitions,
Path extends keyof Definitions,
Method extends keyof Definitions[Path]
> = Definitions[Path][Method] extends { response: infer R } ? R : never;
// ---------- 类型安全的 API 客户端 ----------
type AvailablePaths = keyof APIDefinitions;
// '/users' | '/users/:id' | '/posts'
// 为指定路径和方法获取类型
type GetUsersResponse = ResponseType<APIDefinitions, '/users', 'GET'>;
// { users: Array<{ id: number; name: string }>; total: number }
type CreateUserBody = ExtractMethod<APIDefinitions['/users'], 'POST'>;
// { body: { name: string; email: string }; response: { id: number; ... } }
// ---------- 构建通用请求函数 ----------
type MethodName = 'GET' | 'POST' | 'PUT' | 'DELETE';
function apiClient<
Path extends AvailablePaths,
Method extends MethodName & keyof APIDefinitions[Path]
>(
path: Path,
method: Method,
params?: RequestParams<APIDefinitions, Path, Method>,
): Promise<ResponseType<APIDefinitions, Path, Method>> {
// 实际实现省略,这里是类型验证
return fetch(path, { method }).then(r => r.json());
}
// ---------- 使用验证 ----------
async function testApiClient() {
// 获取用户列表
const users = await apiClient('/users', 'GET', { page: 1, limit: 20 });
// users 的类型为 { users: Array<{ id: number; name: string }>; total: number }
console.log(users.users[0].name); // string ✓
// 创建用户
const newUser = await apiClient('/users', 'POST', { name: 'Alice', email: 'alice@test.com' });
// newUser 的类型为 { id: number; name: string; email: string }
console.log(newUser.id); // number ✓
// 删除用户(路径参数已在模板中,但这里需要实际路径)
const result = await apiClient('/users/:id', 'DELETE');
// ❌ 上面这个类型是正确的,但实际调用时 path 应为 /users/1
// 以下调用会被 TypeScript 阻止:
// await apiClient('/users', 'GET', { name: 'test' }); // ❌ page/limit 才有,name 没有
// await apiClient('/users', 'DELETE', {}); // ❌ DELETE 不在 /users 的 methods 中
}
// ---------- 进阶:URL 参数模板解析 ----------
// 提取 /users/:id → 要求 id 参数
type RouteParams<Path extends string> =
Path extends `${string}:${infer P}/${infer Rest}`
? { [K in P | keyof RouteParams<Rest>]: string }
: Path extends `${string}:${infer P}`
? { [K in P]: string }
: {};
type UserIdParams = RouteParams<'/users/:id'>;
// { id: string }
type PostCommentParams = RouteParams<'/posts/:postId/comments/:commentId'>;
// { postId: string; commentId: string }
// 综合:完整的 API 调用函数(包含路径参数替换 + query 参数 + body)
async function apiCall<
Path extends AvailablePaths,
Method extends MethodName & keyof APIDefinitions[Path]
>(
pathTemplate: Path,
method: Method,
options: RequestParams<APIDefinitions, Path, Method> & RouteParams<Path>
): Promise<ResponseType<APIDefinitions, Path, Method>> {
// 1. 替换路径模板中的动态参数
let resolvedPath: string = pathTemplate;
const pathParams = {} as RouteParams<Path>;
// ... 路径替换实现
// 2. 发送请求
const response = await fetch(resolvedPath, { method, body: JSON.stringify(options) });
return response.json();
}
六、底层原理
6.1 TypeScript 编译器如何执行条件类型
条件类型的执行在 TypeScript 编译器内部分为几个阶段:
阶段 1:实例化(Instantiation)
当编译器遇到 MyReturnType<() => string>:
- 查找
MyReturnType定义:type MyReturnType<T> = T extends (...args: any) => infer R ? R : any - 绑定
T = () => string - 计算
extends条件:() => string是否可赋值给(...args: any) => any?→ 是
阶段 2:模式匹配(Pattern Matching)
当条件为 true 时,编译器尝试匹配 infer 位置:
1
2
3
4
5
6
7
T = () => string
模板 = (...args: any) => infer R
匹配过程:
1. 检查函数签名的参数结构:(...args: any) 匹配 () → 成功
2. 检查函数签名的返回值结构:infer R 匹配 string → R = string
3. 绑定 R = string
阶段 3:替换结果
1
2
infer R 被绑定为 string
整个条件类型的结果 = string(true 分支)
如果 infer 的位置无法匹配:
1
2
3
4
type MyReturnType<T> = T extends (...args: any) => infer R ? R : any;
type Test = MyReturnType<string>;
// string 不能赋值给 (...args: any) => any → false 分支 → any
6.3 条件类型的递归深度限制
TypeScript 对条件类型的递归有深度限制(默认 50 层):
1
2
3
4
5
6
type Recursive<T> = T extends number
? Recursive<Exclude<T, 1>>
: T;
// type Res = Recursive<1 | 2 | 3 | ... | 100>;
// 递归超过 50 层 → Type instantiation is excessively deep and possibly infinite. (2589)
可以通过 --noErrorTruncation 或提高 --typeRecursionLimit(TypeScript 5.0+)来调整限制。但在实际项目中,如果递归深度超过 50 层,通常说明类型设计有问题。
6.5 泛型函数、重载与 infer
infer 处理重载函数时,只匹配最后一个签名(TypeScript 3.4+):
1
2
3
4
5
6
7
8
9
10
function overloaded(x: string): string;
function overloaded(x: number): number;
function overloaded(x: any): any { return x; }
type RT = ReturnType<typeof overloaded>;
// 结果是 string | number 还是 any?
// 答案是 any——因为实现签名是 (x: any) => any
// 但 TypeScript 3.4+ 对此做了优化,实际上返回
// 所有重载返回值的联合:string | number
注意:Parameters<T> 和 ReturnType<T> 在处理重载时会选择最晚的重载签名作为推断依据,但实际返回值类型是所有重载的合并。
七、高频面试题解析
面试题 1:实现一个 MyReturnType<T> 并解释 infer 的行为。为什么不能写成 T extends (...args: any[]) => infer R 这样也能推断出泛型函数的返回类型?
深度解答:
1
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
先回答第一个问题,为什么可以推断出返回值:
当编译器匹配 T extends (...args: any) => infer R 时,它在做的是函数签名的模式匹配:
- 左边 T 是实际的函数类型
- 右边是带 infer 的模式
- 如果 T 可以赋值给模式,infer 位置的类型就被绑定
关于泛型函数的特殊情况:
1
2
3
4
function identity<T>(arg: T): T { return arg; }
type IdentityType = typeof identity;
type RT = MyReturnType<IdentityType>; // unknown
为什么会是 unknown?因为 identity 的类型是 <T>(arg: T) => T,编译器无法从空状态推断出 T 的具体类型。此时 infer R 被绑定为 unknown(泛型参数在未指定时的默认值)。
1
2
3
// 解决方式:指定泛型参数
type IdentityString = typeof identity<string>; // (arg: string) => string
type RTFixed = MyReturnType<IdentityString>; // string
面试题 2:实现 UnwrapPromise<T>,将 Promise<Promise<...<T>>> 递归解包为最内层类型。要求支持深度嵌套且能处理非 Promise 类型。
深度解答:
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
type UnwrapPromise<T> = T extends Promise<infer U>
? UnwrapPromise<U> // 递归解包
: T; // 非 Promise 类型直接返回
// 验证
type P1 = UnwrapPromise<Promise<string>>; // string
type P2 = UnwrapPromise<Promise<Promise<number>>>; // number
type P3 = UnwrapPromise<Promise<Promise<Promise<boolean>>>>; // boolean
type P4 = UnwrapPromise<string>; // string(非 Promise 直接返回)
type P5 = UnwrapPromise<Promise<string | number>>; // string | number
// 进阶:同时处理可迭代的 Promise(如 Promise<Promise<string[]>>)
type UnwrapDeep<T> = T extends Promise<infer U>
? U extends Promise<any>
? UnwrapDeep<U>
: U
: T;
// 边界情况处理
type UnwrapSafe<T> = T extends Promise<infer U>
? UnwrapSafe<U>
: T extends any[]
? { [K in keyof T]: UnwrapSafe<T[K]> }
: T;
// 测试边界
type TU = UnwrapSafe<Promise<[number, Promise<string>]>>;
// [number, string]
关键点:
- 递归终止条件:当 T 不是 Promise 类型时,返回 T 自身
- 递归深度:TypeScript 默认 50 层限制,很少触发
- 分布式处理:
Promise<string | number>不会被分布——因为 Promise 是泛型类,不是裸类型参数
面试题 3:实现一个类型工具 UnionToIntersection<T>,将联合类型转换为交叉类型。例如 {a: number} | {b: string} → {a: number} & {b: string}。
深度解答:
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
// 核心思路:利用函数参数逆变的特性
// 当联合类型出现在逆变位置时,infer 会将其推断为交叉类型
type UnionToIntersection<U> =
(U extends any ? (k: U) => void : never) extends (k: infer I) => void
? I
: never;
// 逐步分析:
type Test = UnionToIntersection<{ a: number } | { b: string }>;
// 步骤 1: 分布式条件类型展开
// (U extends any ? (k: U) => void : never)
// 等价于 ((k: {a: number}) => void) | ((k: {b: string}) => void)
// 步骤 2: 将联合函数类型作为整体,匹配 (k: infer I) => void
// 由于函数参数是逆变的,infer I 会将多个参数类型的"或"转变为"与"
// 即 {a: number} & {b: string}
// 验证
type T1 = UnionToIntersection<string | number>; // never(string & number 不可实现)
type T2 = UnionToIntersection<{ name: string } | { age: number }>;
// { name: string } & { age: number } → { name: string; age: number }
type T3 = UnionToIntersection<(() => string) | (() => number)>;
// (() => string) & (() => number)
// 应用:取出对象所有值的联合类型
type ValuesOf<T> = T extends { [K in keyof T]: infer V } ? V : never;
type V1 = ValuesOf<{ a: string; b: number }>; // string | number
解题思路详解:
这个工具类型利用了 TypeScript 类型系统的两个特性:
- 分布式条件类型:
U extends any ? (k: U) => void : never将联合类型分布为多个函数参数的联合 - 逆变推断:infer 在函数参数位置推断时,多个函数类型的参数会被推断为交叉类型
这是 TypeScript 类型体操中最巧妙的技巧之一,也是”高级”难度的经典题型。
八、总结与扩展
核心要点
- 条件类型 = 类型的 if/else:
T extends U ? X : Y - infer = 类型的模式匹配:在条件类型的 extends 子句中声明并绑定类型变量
- 分布式条件类型是双刃剑:裸类型参数自动分布,需要用
[T]包装来阻止 - 逆变/协变影响 infer 结果:参数位置 infer 倾向交叉、返回值位置 infer 倾向联合
使用条件类型的最佳实践
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// ✅ 用 [T] 包装防止非预期的分布式行为
type CheckNever<T> = [T] extends [never] ? true : false;
// ✅ 用多个 infer 提取复杂结构
type ExtractPromise<T> = T extends Promise<infer U> ? U : T;
// ✅ 联合 infer 提取多个信息
type Split<T> = T extends [infer A, ...infer B] ? { first: A; rest: B } : never;
// ❌ 不要在条件类型中放置副作用
// type Bad = T extends string ? console.log('string') : any; // ❌ 类型中没有 console
// ❌ 不要做超出类型系统能力的事情
// 类型系统是编译时的,无法影响运行时行为
从工具类型到真实项目
掌握了条件类型和 infer,你可以编写出类型安全的 API 客户端、Zod/Parsers 的类型推导器、依赖注入的类型容器等高级类型工具。这正是 TypeScript 类型系统”图灵完备”的真正体现。