在字节、阿里等大厂的 TypeScript 面试中,考察工具类型(Utility Types)是一个非常经典的环节。面试官并不只是想看你背诵 Pick 或 Omit 的用法,而是想通过“手写 MyPick”这道题,考察你对泛型(Generics)、索引类型查询(keyof)、映射类型(Mapped Types)以及类型约束(extends)的深度理解。
这篇文章将带你从“知其然”到“知其所以然”,用幽默且硬核的方式彻底拿下这个知识点。
为什么我们需要 Pick?(面试官的潜台词)
在写代码时,我们经常会遇到这种情况:后端定义了一个巨大的 User 对象,包含 id、name、age、password、createdAt 等十几个字段。但在前端的一个小卡片组件里,我只需要展示 name 和 avatar。
如果不使用 Pick,你可能需要重新定义一个接口,或者手动去 extends 然后重写属性。这不仅啰嗦,而且一旦后端改了字段,你的代码维护起来就是灾难。
Pick 的本质:它就像是一个“类型级的过滤器”。你给它一个完整的对象类型,再给它几个你想要的字段名,它就能给你吐出一个全新的、精简的类型。
庖丁解牛:手写 MyPick 的三步走战略
面试官让你在 type MyPick<T, K> = any 的 any 处填空,你该如何思考?我们可以把这个过程拆解为三个步骤:
第一步:明确原材料(泛型参数)
我们需要两个参数:
T:原始的、完整的对象类型(比如User)。K:我们想要挑选出来的属性名(比如'name' | 'age')。
第二步:加上安全锁(类型约束)
这是面试中最容易丢分的地方。如果用户传了一个 T 中不存在的属性怎么办?比如 Pick<User, 'nonExistentField'>。
为了防止这种情况,我们必须限制 K。K 必须是 T 中所有键的集合的子集。
这就引入了 keyof T 和 extends:
keyof T:获取T所有属性名组成的联合类型(例如'id' | 'name' | 'age')。K extends keyof T:这句话的意思是,“K 必须是keyof T的一部分”。如果传了不存在的属性,TypeScript 会直接报错,这就是类型安全。
第三步:加工生产(映射类型)
拿到了合法的 K,我们需要构建新对象。这里要用到映射类型。
语法结构是:{ [P in K]: ... }。
这就像是一个 for...in 循环,遍历 K 中的每一个属性 P,然后去原始类型 T 中查找 P 对应的类型(即 T[P],这叫索引访问类型)。
核心代码实现与逐行精讲
结合上述思路,我们可以写出以下完美的实现代码:
1// 1. 定义原始类型
2interface User {
3 id: number;
4 age: number;
5 name: string;
6 password: string; // 敏感字段
7}
8
9// 2. 手写 MyPick
10// T: 源类型
11// K: 需要挑选的键,且 K 必须受限于 keyof T (即 K 必须是 T 中存在的属性)
12type MyPick<T, K extends keyof T> = {
13 // 映射类型:遍历 K 中的每一个属性 P
14 [P in K]: T[P]; // T[P] 表示取出 T 中 P 属性对应的类型
15}
16
17// 3. 测试
18type UserName = MyPick<User, 'name'>;
19// 结果:{ name: string }
20
21type UserPublicInfo = MyPick<User, 'id' | 'name'>;
22// 结果:{ id: number; name: string; }
23
24// 4. 错误测试 (TypeScript 会报错,因为 'hack' 不在 User 中)
25// type ErrorCase = MyPick<User, 'hack'>;
关键知识点深度解析
为了在面试中对答如流,你需要理解以下几个核心概念:
keyof 操作符
它的作用是“取键”。对于一个对象类型,keyof 会返回它所有属性名的联合类型。
- 例子:
keyof User得到'id' | 'age' | 'name' | 'password'。
索引访问类型
语法是 T[P]。它的作用是“取值”。
- 例子:如果
P是'name',那么User['name']就是string。
映射类型
语法是 { [P in K]: ... }。它允许你将一个联合类型转换为一个新的对象类型。
- 在
MyPick中,我们遍历的是K(用户想要的键),而不是keyof T(所有的键),这就是“挑选”的精髓。
extends 关键字
在这里它不是“继承”,而是“约束”。K extends keyof T 保证了传入的键是合法的。
举一反三:Omit 与 Partial
面试官通常会接着问:“那你能手写一下 Omit 吗?”
其实 Omit 就是 Pick 的反面。Omit 是“排除”某些字段。
它的实现思路是:先利用 Exclude 工具类型从 keyof T 中剔除掉 K,剩下的就是我们要保留的,然后再用 Pick 的逻辑。
1// 手写 Omit
2// Exclude<UnionType, ExcludedMembers> 用于从联合类型中排除某项
3type MyOmit<T, K extends keyof T> = MyPick<T, Exclude<keyof T, K>>;
Partial
Partial 则是将所有属性变为可选。
1type MyPartial<T> = {
2 [P in keyof T]?: T[P];
3}
总结
在面试中回答这道题,建议遵循以下逻辑流:
- 定义泛型:声明
T和K。 - 添加约束:使用
K extends keyof T确保类型安全。 - 构建映射:使用
{ [P in K]: T[P] }完成类型的重组。
掌握了这个模板,你不仅搞定了 Pick,也顺手拿下了 Omit、Readonly 和 Partial,它们是 TypeScript 高级类型编程的基石。