高级类型挑战深度解析
一句话概括
Promise.all 类型定义、柯里化函数类型推导、路径提取类型——这三个高级类型挑战分别考察了变长元组映射、递归条件类型与函数重载推导、模板字面量类型与递归遍历在 TypeScript 类型系统中的应用,是类型编程能力的分水岭。
一、背景与意义
1.1 什么是类型挑战?
类型挑战(Type Challenges)源自 GitHub 上著名的 type-challenges 仓库,包含从 easy 到 extreme 的渐进式题型。它已经成为 TypeScript 社区中评估类型编程能力的标准,也是各大厂面试中常见的”加分题”。
1.2 为什么这三个挑战有代表性?
| 挑战 | 考察能力 | 难度 |
|---|---|---|
Promise.all 类型 | 变长元组映射、条件类型 | medium |
| 柯里化函数类型 | 递归类型、函数重载推导 | hard |
| 路径提取类型 | 模板字面量、递归遍历 | extreme |
这三个挑战覆盖了 TypeScript 类型编程的三大核心能力:映射类型的高级用法、递归类型推导、模板字面量类型。掌握了它们,就掌握了 90% 的类型体操场景。
1.3 为什么面试常考
Promise.all类型:考察泛型变长参数(variadic tuple types)的理解,实际项目中大量使用 Promise 并发- 柯里化函数类型:考察递归类型和函数签名推导,体现了对函数式编程范式的理解
- 路径提取类型:考察模板字面量和递归遍历,在 i18n 配置、表单校验中广泛应用
二、概念与定义
2.1 变长元组类型(Variadic Tuple Types)
TypeScript 4.0+ 支持变长元组类型:
1
2
3
4
5
6
7
8
9
10
11
12
// 定长元组
type Fixed = [string, number, boolean];
// 变长元组(TS 4.0+)
type Variable = [string, ...number[]]; // 第一个元素是 string,后续任意长度 number
// 泛型变长元组
type Push<T extends any[], V> = [...T, V];
type PushResult = Push<[1, 2, 3], 4>; // [1, 2, 3, 4]
type Shift<T extends any[]> = T extends [infer F, ...infer R] ? R : [];
type ShiftResult = Shift<[1, 2, 3]>; // [2, 3]
2.2 柯里化(Currying)
柯里化是将一个多参数函数转换为一系列单参数函数的过程:
1
2
3
4
5
6
7
8
9
10
11
12
13
// 普通函数
function add(a: number, b: number, c: number): number {
return a + b + c;
}
// 柯里化后
const curriedAdd = (a: number) => (b: number) => (c: number): number => a + b + c;
curriedAdd(1)(2)(3); // 6
// 部分应用
const add5 = curriedAdd(5);
const add5And3 = add5(3);
add5And3(2); // 10
柯里化在函数式编程中非常常见,但给 TypeScript 类型推导带来了挑战——因为参数数量是未知的,我们需要递归地推导出每次调用返回的函数类型。
2.3 模板字面量类型(Template Literal Types)
TypeScript 4.1+ 引入了模板字面量类型:
1
2
3
4
5
type EventName = `on${Capitalize<string>}`;
// 任何以 "on" 开头的字符串
type Route = `/users/${string}/posts/${string}`;
// 匹配 /users/xxx/posts/xxx 格式
结合 infer,模板字面量可以提取路径参数:
1
2
3
4
5
6
7
type ExtractParam<S extends string> =
S extends `/users/${infer Id}/posts/${infer PostId}`
? { userId: Id; postId: PostId }
: never;
type Params = ExtractParam<'/users/42/posts/100'>;
// { userId: '42'; postId: '100' }
三、最小示例:三个挑战的手写实现
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
// ========== 挑战 1: Promise.all 类型定义 ==========
// 目标:写一个类型,使得 Promise.all([p1, p2, p3]) 能正确推导每个 Promise 的解包类型
// 方法一:使用 mapped types over tuple(推荐,TS 3.1+)
declare function PromiseAll<T extends readonly any[]>(
values: readonly [...T]
): Promise<{
[K in keyof T]: T[K] extends Promise<infer R> ? R : T[K];
}>;
// 方法二:使用递归条件类型(也可行,但代码更复杂)
type UnwrapPromises<T extends any[]> =
T extends [infer F, ...infer R]
? [F extends Promise<infer U> ? U : F, ...UnwrapPromises<R>]
: [];
declare function PromiseAll2<T extends any[]>(
values: readonly [...T]
): Promise<UnwrapPromises<T>>;
// ========== 挑战 2: 柯里化函数类型 ==========
// 目标:给定函数类型 F,推导出其柯里化版本的类型
type Curried<F extends (...args: any[]) => any> =
F extends (...args: []) => infer R
? () => R // 空参数 → 直接返回
: F extends (...args: [infer A]) => infer R
? (arg: A) => R // 单参数 → 返回结果
: F extends (...args: [infer A, ...infer Rest]) => infer R
? (arg: A) => Curried<(...args: Rest) => R> // 多参数 → 递归
: never;
// 重写为更健壮的版本(处理参数展开等情况)
type Curry<F extends (...args: any) => any> =
F extends (...args: infer P) => infer R
? P extends [infer A, ...infer Rest]
? Rest extends []
? (arg: A) => R
: (arg: A) => Curry<(...args: Rest) => R>
: () => R
: never;
// 柯里化函数的实现(类型安全的版本)
function curry<T extends (...args: any[]) => any>(
fn: T
): Curry<T> {
return function curried(...args: any[]) {
if (args.length >= fn.length) {
return fn(...args);
}
return (...nextArgs: any[]) => curried(...args, ...nextArgs);
} as any;
}
// ========== 挑战 3: 路径提取类型 ==========
// 目标:给定一个嵌套对象类型,提取所有可能的属性路径(如 'a.b.c')
// 第一步:简单版本——提取一层路径
type SimplePath<T> = T extends object
? { [K in keyof T]: K extends string ? `${K}` | `${K}.${SimplePath<T[K]>}` : never }[keyof T]
: never;
// 第二步:完整版本——路径提取,支持 {} 终止,过滤 never
type Path<T, Prefix extends string = ''> =
T extends Record<string, unknown>
? {
[K in keyof T & string]:
| `${Prefix}${K}`
| Path<T[K], `${Prefix}${K}.`>;
}[keyof T & string]
: never;
// 第三步:带值类型的路径提取
type PathValue<T, P extends string> =
P extends `${infer K}.${infer Rest}`
? K extends keyof T
? PathValue<T[K], Rest>
: never
: P extends keyof T
? T[P]
: never;
// ========== 验证 ==========
// Promise.all 验证
const p1 = Promise.resolve(42);
const p2 = Promise.resolve('hello');
const p3 = Promise.resolve(true);
async function testPromiseAll() {
const result = await PromiseAll([p1, p2, p3] as const);
// result 类型: [number, string, boolean]
const n: number = result[0]; // ✅
const s: string = result[1]; // ✅
const b: boolean = result[2]; // ✅
}
// 柯里化验证
function sum(a: number, b: number, c: number): number {
return a + b + c;
}
type CurriedSum = Curry<typeof sum>;
// (arg: number) => (arg: number) => (arg: number) => number
const curriedSum = curry(sum);
const result = curriedSum(1)(2)(3); // 类型正确推导为 number
// 路径提取验证
interface DeepObject {
user: {
name: string;
profile: {
age: number;
address: {
city: string;
zip: string;
};
};
};
settings: {
theme: 'light' | 'dark';
notifications: boolean;
};
}
type AllPaths = Path<DeepObject>;
// "user" | "user.name" | "user.profile" | "user.profile.age" | "user.profile.address" |
// "user.profile.address.city" | "user.profile.address.zip" |
// "settings" | "settings.theme" | "settings.notifications"
type CityType = PathValue<DeepObject, 'user.profile.address.city'>;
// string
type ThemeType = PathValue<DeepObject, 'settings.theme'>;
// 'light' | 'dark'
四、核心知识点拆解
4.1 Promise.all 类型的映射技巧
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Promise.all([p1, p2, p3]) 的类型推导
// 输入:[Promise<number>, Promise<string>, Promise<boolean>]
// 输出:Promise<[number, string, boolean]>
// 关键语法:映射类型在元组上的应用
type MapTuple<T extends any[]> = {
[K in keyof T]: T[K] extends Promise<infer R> ? R : T[K];
};
// 这利用了 TypeScript 的映射类型同态特性——
// 当映射类型应用于元组时,结果也是元组
// 即 { [K in keyof [number, string]]: ... } → [..., ...]
// 但如果只是 T[K] 而没有条件判断
type Identity<T extends any[]> = { [K in keyof T]: T[K] };
// 对于 Identity<[number, string]>,结果应该是 [number, string] 还是 { 0: number; 1: string }?
// TypeScript 会保留元组结构!
// 关键点:映射类型 + keyof T 时,如果 T 是元组/数组,结果保留其结构
as const 的重要性:
1
2
3
4
5
6
7
8
9
10
// 不加 as const 时,数组字面量的类型是 (string | number | boolean)[]
const arr = [Promise.resolve(1), Promise.resolve('a'), Promise.resolve(true)];
// arr 的类型: (Promise<number> | Promise<string> | Promise<boolean>)[]
// 加 as const 时,数组字面量的类型是元组
const arrAsConst = [Promise.resolve(1), Promise.resolve('a'), Promise.resolve(true)] as const;
// arrAsConst 的类型: readonly [Promise<number>, Promise<string>, Promise<boolean>]
// 所以 Promise.all 需要接收 readonly 数组/tuple
declare function PromiseAll<T extends readonly any[]>(values: T): Promise<...>;
4.2 柯里化类型推导的递归逻辑
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 Curry<F extends (...args: any) => any> =
F extends (...args: infer P) => infer R
? P extends [infer A, ...infer Rest]
? Rest extends []
? (arg: A) => R // 最后一步:返回结果
: (arg: A) => Curry<(...args: Rest) => R> // 还有更多参数:递归
: () => R // 无参数:直接返回
: never;
// 以 (a: number, b: string, c: boolean) => string 为例:
// 1. F = (a: number, b: string, c: boolean) => string
// P = [number, string, boolean], R = string
// P extends [infer A, ...infer Rest] → A = number, Rest = [string, boolean]
// Rest 不是空数组 → (arg: A) => Curry<(...args: [string, boolean]) => string>
// 2. Curry<(...args: [string, boolean]) => string>
// P = [string, boolean], R = string
// A = string, Rest = [boolean](不是空数组)
// → (arg: string) => Curry<(...args: [boolean]) => string>
// 3. Curry<(...args: [boolean]) => string>
// P = [boolean], R = string
// A = boolean, Rest = [] → 空数组!
// → (arg: boolean) => string
// 最终结果:
// (a: number) => (b: string) => (c: boolean) => string
参数默认值问题:
1
2
3
4
5
6
7
function fn(a: number, b: string = 'default'): string {
return `${a}-${b}`;
}
// len = 1(有默认值的参数不计入函数长度)
// 柯里化时,应该允许只传一个参数就立即执行
// 这需要在类型推导中考虑默认参数
4.3 路径提取类型的递归遍历
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
type Path<T, Prefix extends string = ''> =
T extends Record<string, unknown>
? {
[K in keyof T & string]:
| `${Prefix}${K}` // 当前路径名
| Path<T[K], `${Prefix}${K}.`>; // 递归子路径
}[keyof T & string] // 取所有值的联合
: never;
// 以 { user: { name: string; age: number } } 为例:
// 1. Prefix = ''
// T = { user: { name: string; age: number } }
// K in 'user':
// → `${''}user` = 'user'
// → Path<{ name: string; age: number }, 'user.'>
// 2. Prefix = 'user.'
// T = { name: string; age: number }
// K in 'name' | 'age':
// → 'name': 'user.name' | Path<string, 'user.name.'> → 'user.name'
// → 'age': 'user.age' | Path<number, 'user.age.'> → 'user.age'
// (number 不继承 Record<string, unknown>,所以 Path 返回 never)
// 最终:'user' | 'user.name' | 'user.age'
为什么需要 ${} 包装?因为映射类型的值会生成联合类型,而我们将路径作为值:
1
2
3
4
5
6
type Test = {
[K in 'a' | 'b']: `prefix.${K}`;
};
// { a: 'prefix.a'; b: 'prefix.b' }
// Test[keyof Test] = 'prefix.a' | 'prefix.b'
4.4 这三种挑战的共性
三个挑战都使用了递归条件类型:
| 挑战 | 递归主体 | 终止条件 |
|---|---|---|
| Promise.all | 遍历元组每个元素 | 元组末尾 |
| 柯里化 | 逐个消耗参数 | 参数列表为空 |
| 路径提取 | 遍历对象嵌套层 | 值类型不是 Record<string, unknown> |
这是类型编程中递归的三种典型模式:遍历列表、消耗参数、遍历树。
五、实战案例:类型安全的数据访问层
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
// ========== 场景:构建类型安全的嵌套数据访问 ==========
// 数据库 schema 定义
interface DatabaseSchema {
users: Array<{
id: number;
name: string;
email: string;
profile: {
bio: string;
avatar: string;
social: {
twitter: string;
github: string;
};
};
settings: {
theme: 'light' | 'dark';
language: 'zh' | 'en';
notifications: {
email: boolean;
push: boolean;
};
};
posts: Array<{
id: number;
title: string;
content: string;
createdAt: string;
tags: string[];
}>;
}>;
}
// ---------- 1. 路径类型定义 ----------
// 提取所有可能的路径
type DBRoot = DatabaseSchema['users'][number]; // 单用户类型
// 从特定节点开始的路径
type UserPaths = Path<DBRoot>;
// "id" | "name" | "email" | "profile" | "profile.bio" | ... 等
// ---------- 2. 使用路径实现类型安全的 getter ----------
function get<T, P extends Path<T>>(obj: T, path: P): PathValue<T, P> {
const keys = (path as string).split('.');
let current: any = obj;
for (const key of keys) {
if (current === undefined || current === null) {
throw new Error(`Path ${path} failed at key ${key}`);
}
current = current[key];
}
return current as PathValue<T, P>;
}
// 使用
const user: DBRoot = {
id: 1,
name: 'Alice',
email: 'alice@test.com',
profile: {
bio: 'Developer',
avatar: '/avatar.png',
social: { twitter: '@alice', github: 'alice-dev' },
},
settings: { theme: 'dark', language: 'en', notifications: { email: true, push: false } },
posts: [{ id: 1, title: 'Hello', content: 'World', createdAt: '2024-01-01', tags: ['ts'] }],
};
const twitter = get(user, 'profile.social.twitter'); // string ✅
const theme = get(user, 'settings.theme'); // 'light' | 'dark' ✅
// const invalid = get(user, 'profile.nonexistent'); // ❌ Error: 路径不存在
console.log(twitter); // "@alice"
console.log(theme); // "dark"
// ---------- 3. 类型安全的 set 函数 ----------
// 设置特定路径的值
function set<T, P extends Path<T>>(
obj: T,
path: P,
value: PathValue<T, P>
): T {
const keys = (path as string).split('.');
const lastKey = keys.pop()!;
let current: any = obj;
for (const key of keys) {
current = current[key];
}
current[lastKey] = value;
return obj;
}
set(user, 'profile.social.github', 'alice-updated');
// ✅ 类型安全:第二个参数接受 string
// set(user, 'settings.theme', 'blue'); // ❌ Error: 'blue' 不是 'light' | 'dark'
// ---------- 4. Flatten 工具:将嵌套对象展平为单层 ----------
type Flatten<T, Prefix extends string = ''> = {
[K in Path<T> as K extends string ? K : never]: PathValue<T, K>;
};
type FlatUser = Flatten<DBRoot>;
// {
// id: number;
// name: string;
// email: string;
// 'profile.bio': string;
// 'profile.avatar': string;
// 'profile.social.twitter': string;
// 'profile.social.github': string;
// 'settings.theme': 'light' | 'dark';
// 'settings.language': 'zh' | 'en';
// 'settings.notifications.email': boolean;
// 'settings.notifications.push': boolean;
// ...
// }
// ---------- 5. 类型安全的更新函数(部分更新)----------
type DeepPartialByPath<T> = {
[P in Path<T>]?: PathValue<T, P>;
};
function update<T>(obj: T, patches: DeepPartialByPath<T>): T {
const result = { ...obj };
for (const [path, value] of Object.entries(patches)) {
if (value !== undefined) {
set(result, path as Path<T>, value as any);
}
}
return result;
}
const updatedUser = update(user, {
'profile.bio': 'Senior Developer',
'settings.theme': 'light',
'profile.social.github': 'alice-github',
});
// ✅ 类型安全:只允许已知路径
六、底层原理
6.1 TypeScript 对变长元组的内部处理
变长元组(Variadic Tuples)是 TypeScript 4.0 的标志性特性。它的本质是在元组类型中引入展开运算符(spread)的泛型支持。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 内部表示
type Fixed = [string, number, boolean];
// 内部:TupleType([StringType, NumberType, BooleanType])
type Variable = [string, ...number[]];
// 内部:TupleType([StringType], NumberType[]) — 固定前缀 + 变长后缀
// 泛型变长元组
type Push<T extends unknown[], V> = [...T, V];
// 内部:编译器在处理 Push 时
// 1. 解析 T 为 TupleType([...]) 或 ArrayType
// 2. 创建新的元组类型,将 T 的元素与 V 组合
// 3. 如果 T 是具体类型 [1, 2, 3],则 Push 结果为 [1, 2, 3, V]
// 4. 如果 T 是泛型参数,则 Push 结果为 Lazy([...T, V])
面试要点:映射类型
{ [K in keyof T]: ... }作用于元组时,TypeScript 会自动保留元组的固定+rest 结构。这正是PromiseAll类型能从[Promise<number>, Promise<string>]→[number, string]的原理——同态映射保留元组结构。七、高频面试题解析
面试题 1:实现 PromiseAll<T> 类型,要求输入 [Promise<number>, Promise<string>] 输出 [number, string]。 为什么用 { [K in keyof T]: ... } 能保留元组结构?
深度解答:
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
type MyPromiseAll<T extends readonly any[]> = {
[K in keyof T]: T[K] extends Promise<infer R> ? R : T[K];
};
// 为什么能保留元组结构?
// 这利用了 TypeScript 的 "Homomorphic Mapped Types"(同态映射类型)特性
// 规则:如果映射类型的结构是 { [P in keyof T]: ... }
// 且 T 是数组或元组类型,则结果也会是数组或元组类型
// 验证:
type Test = { [K in keyof [number, string, boolean]]: [number, string, boolean][K] };
// 结果:[number, string, boolean](保留了元组结构)
// 而不是 { 0: number; 1: string; 2: boolean; length: 3; ... }
// 与之对比,非同态映射不保留:
type NonHomomorphic<K extends PropertyKey, V> = { [P in K]: V };
type Test2 = NonHomomorphic<keyof [1, 2], string>;
// { 0: string; 1: string; length: string; toString: string; ... } ❌ 不是元组
// 完整定义(支持 readonly 和非 Promise 值):
declare function PromiseAll<T extends readonly any[]>(
values: T
): Promise<{
-readonly [K in keyof T]: T[K] extends Promise<infer R> ? R : T[K];
}>;
面试题 2:实现 Curry<T> 类型,并解释为什么递归条件类型在此处能工作。
深度解答:
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
type Curry<F extends (...args: any) => any> =
F extends (...args: infer P) => infer R
? P extends [infer A, ...infer Rest]
? Rest extends []
? (arg: A) => R // 终止:最后一步
: (arg: A) => Curry<(...args: Rest) => R> // 递归:还有更多参数
: () => R // 无参数
: never;
// 递归的工作过程:
// 1. 从 F 的完整参数列表 P 开始
// 2. 用 [infer A, ...infer Rest] 拆解出第一个参数 A 和剩余参数 Rest
// 3. 如果 Rest 为空 → 返回 (arg: A) => R(最后一步)
// 4. 如果 Rest 非空 → 返回 (arg: A) => Curry<(...args: Rest) => R>
// 5. 在 Curry 的递归调用中,Rest 成为新的参数列表
// 6. 不断消耗参数,直到 Rest 为空
// 边界情况处理:
// 1. 无参数的函数:() => R → () => R
type NoArgs = Curry<() => string>; // () => string
// 2. 单参数:() => R
type OneArg = Curry<(x: number) => string>; // (arg: number) => string
// 3. 多参数
type ThreeArgs = Curry<(a: string, b: number, c: boolean) => string>;
// (a: string) => (b: number) => (c: boolean) => string
// 4. rest 参数
type RestArgs = Curry<(...args: number[]) => string>;
// (...args: number[]) => string(rest 参数无法递归—因为无法分解出第一个元素)
// 5. 可选参数
type OptionalArgs = Curry<(a: string, b?: number) => string>;
// 可选参数会使得 P 的类型变为 [string, number?]
// Rest = [number?]
// 当 Curry 递归时,(...args: [number?]) => string 的参数列表包含可选参数
// 结果:(a: string) => (b?: number) => string
面试题 3:实现对象路径提取类型 Path<T>,并解释为什么需要 [keyof T & 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
31
32
33
34
35
36
37
38
39
40
41
42
43
type Path<T, Prefix extends string = ''> =
T extends Record<string, unknown>
? {
[K in keyof T & string]:
| `${Prefix}${K}`
| Path<T[K], `${Prefix}${K}.`>;
}[keyof T & string]
: never;
// 逐步拆解:
// 1. `T extends Record<string, unknown>` 的作用
// 只对对象类型递归,过滤掉 string、number 等基本类型和数组
// 数组有数字索引,但我们只关心字符串键
// 2. `K in keyof T & string` 的作用
// keyof { a: string; b: number; 0: boolean } = 'a' | 'b' | 0
// `string & 0` → never(数字和字符串没有交集)
// 所以 `keyof T & string` 过滤掉了数字索引
// 结果:只包含字符串键 'a' | 'b'
// 3. `{ [K in ...]: ... }[keyof T & string]` 的作用
// 这被称为"查表取值"(Indexed Access on Mapped Type)
// { a: 'x'; b: 'y' }['a' | 'b'] → 'x' | 'y'
// 将所有路径值合并为联合类型
// 4. 为什么需要这个语法?
// 如果不加 `[keyof T & string]`,你得到的是 { K: 路径字符串 } 对象
// 加上后,你得到的是所有路径字符串的联合类型
// 即从映射对象的值中提取出联合
// 5. 类型参数和类型变量的区别
// 在 Path<T[K]> 中,T[K] 是新的类型,递归调用 Path
// Prefix 是累积的字符串前缀
// 实例:
// Path<{ a: { b: string; c: number } }>
// K = 'a':
// `${''}a` = 'a'
// Path<{ b: string; c: number }, 'a.'>
// K = 'b': 'a.b'
// K = 'c': 'a.c'
// 结果:'a' | 'a.b' | 'a.c'
八、总结与扩展
三个挑战的启示
| 挑战 | 核心技巧 | 实际应用 |
|---|---|---|
| Promise.all | 映射类型同态性 + 条件类型 | 类型安全的并发 API |
| 柯里化 | 递归条件类型 + 参数列表分解 | 函数式工具库、部分应用 |
| 路径提取 | 递归映射 + 模板字面量 + 查表取值 | i18n、表单校验、配置管理 |
从挑战到真实类型系统
掌握了这三个挑战,你就具备了编写以下高级类型的能力:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 1. 类型安全的 Redux createSlice
type Slice<T, K extends keyof T> = {
[P in keyof T[K]]: (state: T, action: { payload: T[K][P] }) => void;
};
// 2. 可递归的 Omit(深度排除)
type DeepOmit<T, K extends string> = T extends Record<string, unknown>
? {
[P in keyof T as P extends K ? never : P]: DeepOmit<T[P], K>;
}
: T;
// 3. 可组合的守卫类型
type Guard<T, U> = T extends U ? T : never;
// 4. 类型安全的深度克隆
type DeepCloneable<T> = T extends object
? { [P in keyof T]: DeepCloneable<T[P]> }
: T;
这些挑战不仅仅是面试题——它们是 TypeScript 类型系统表达能力的证明。当你能够用类型系统来表达这些复杂的关系时,你就能写出越来越自文档化、自验证的代码。