TypeScript 工具类型怎么记:从 Pick、Omit 到 keyof 与 Exclude 推导
学习 TypeScript 时,很多人能背出 Pick、Omit、Partial,遇到组合题却仍会把"保留"和"删除"写反。关键不是继续背定义,而是先判断工具类型的输入是什么、输出是什么 。本文用一个 User 类型串起常见工具类型,并重点推导 keyof → Exclude → Pick 如何构成 Omit。
本文根据现有 TypeScript 学习代码进行静态分析,代码编译运行未验证。
一张表区分常见工具类型
先记住它们各自处理什么:
| 工具 | 输入 | 输出 | 核心作用 |
|---|---|---|---|
keyof T |
对象类型 | 属性名联合类型 | 取出全部键 |
Pick<T, K> |
对象类型、键 | 对象类型 | 保留指定属性 |
Omit<T, K> |
对象类型、键 | 对象类型 | 删除指定属性 |
Exclude<T, U> |
联合类型、待排除类型 | 联合类型 | 排除联合成员 |
Partial<T> |
对象类型 | 对象类型 | 全部属性变为可选 |
Required<T> |
对象类型 | 对象类型 | 全部属性变为必填 |
Record<K, V> |
键类型、值类型 | 对象映射类型 | 约束键和值 |
ReturnType<F> |
函数类型 | 返回值类型 | 提取函数返回类型 |
最容易混淆的是:Exclude 处理联合类型,Omit 处理对象属性。
用 User 理解 Pick 与 Omit
基础类型如下:
typescript
interface User {
id: number;
name: string;
age: number;
email: string;
}
Pick:保留指定属性
typescript
type UserPreview = Pick<User, 'id' | 'name'>;
const user: UserPreview = {
id: 1,
name: '张三',
};
Pick<User, 'id' | 'name'> 可以读成:从 User 中挑出 id 和 name。其中第二个参数必须是 User 已有的键。
Omit:删除指定属性
typescript
type UserSafe = Omit<User, 'email'>;
const userSafe: UserSafe = {
id: 1,
name: '张三',
age: 18,
};
Omit<User, 'email'> 删除 email,保留其余属性。记忆时只抓住方向:
Pick的第二个参数是"要保留的键";Omit的第二个参数是"要删除的键"。
keyof 与 Exclude 分别做什么
keyof:对象键变成联合类型
typescript
type UserKeys = keyof User;
// 'id' | 'name' | 'age' | 'email'
这里的输出已经不是对象,而是属性名组成的联合类型。
Exclude:从联合类型排除成员
typescript
type KeepKeys = Exclude<UserKeys, 'email'>;
// 'id' | 'name' | 'age'
Exclude<T, U> 会从联合类型 T 中排除可赋给 U 的成员。因此,它适合处理键的联合类型,但不能直接代替 Omit 处理对象。
Omit 为什么能由 keyof、Exclude、Pick 组成
核心思想可以写成:
typescript
type MyOmit<T, K extends keyof T> =
Pick<T, Exclude<keyof T, K>>;
以删除 email 为例,类型数据流分三步:
text
User
↓ keyof
'id' | 'name' | 'age' | 'email'
↓ Exclude<..., 'email'>
'id' | 'name' | 'age'
↓ Pick<User, ...>
{ id: number; name: string; age: number }
对应代码:
typescript
type AllKeys = keyof User;
type RemainingKeys = Exclude<AllKeys, 'email'>;
type UserWithoutEmail = Pick<User, RemainingKeys>;
因此,理解 Omit 的重点不是背一行实现,而是理解三个阶段:
keyof把对象转换为键的联合类型;Exclude从联合类型中删除不要的键;Pick根据剩余键重新构造对象类型。
高频易错题:KeepKeys 传给 Omit 会怎样
下面这段代码非常容易判断错误:
typescript
type UserKeys = keyof User;
type KeepKeys = Exclude<UserKeys, 'email'>;
type UserOmit = Omit<User, KeepKeys>;
KeepKeys 的值是:
typescript
'id' | 'name' | 'age'
但把它传给 Omit 后,这些键会被删除,所以实际结果是:
typescript
type UserOmit = {
email: string;
};
错误根因是变量名表达"要保留的键",但 Omit 的第二个参数表达"要删除的键"。
如果要保留 KeepKeys,应该写:
typescript
type UserPicked = Pick<User, KeepKeys>;
如果只是排除 email,应该直接写:
typescript
type UserWithoutEmail = Omit<User, 'email'>;
Partial 与 Required:控制属性是否必填
Partial:全部属性变为可选
typescript
type PartialUser = Partial<User>;
const patch: PartialUser = {
name: '李四',
};
它适合描述"只更新部分字段"的参数。Partial<User> 不代表只保留部分字段,而是让每个字段都可以不传。
Required:全部属性变为必填
typescript
interface UserForm {
name?: string;
email?: string;
}
type CompleteUserForm = Required<UserForm>;
Required<T> 主要移除属性上的可选修饰符。需要注意:它不等于无条件删除值类型中显式声明的 undefined。
Record:描述键值映射
Record<K, V> 用来创建"键为 K、值为 V"的对象类型:
typescript
type ErrorMessages = Record<number, string>;
const errorMessages: ErrorMessages = {
400: '请求参数错误',
401: '未登录,请先登录',
403: '没有权限访问',
404: '资源找不到',
500: '服务器错误',
};
结合空值合并运算符 ??,可以提供默认消息:
typescript
function getErrorMessage(code: number): string {
return errorMessages[code] ?? '未知错误';
}
?? 只在左侧为 null 或 undefined 时返回右侧值。
状态码分类中,1xx 更准确地说是信息响应,2xx 才是成功响应。
ReturnType:从函数提取返回类型
typescript
function getPoint() {
return { x: 1, y: 2 };
}
type Point = ReturnType<typeof getPoint>;
// { x: number; y: number }
这里有两步:
typeof getPoint在类型上下文中取得函数类型;ReturnType从函数类型中提取返回值类型。
面试回答模板
面试官问:"Omit 是怎么实现的?"可以这样回答:
Omit<T, K>用于从对象类型T中删除键K。它的核心思路是先通过keyof T得到全部属性名,再用Exclude排除K,最后用Pick根据剩余键构造新对象类型,因此可以理解为Pick<T, Exclude<keyof T, K>>。其中Exclude操作联合类型,Pick和Omit操作对象属性。
如果继续追问 Pick 和 Omit 的区别:
Pick的第二个参数表示保留哪些键,Omit的第二个参数表示删除哪些键。判断组合题时应先算出键的联合类型,再确认这些键传入的是Pick还是Omit。
最终自检清单
遇到工具类型题时,按下面顺序检查:
- 当前输入是对象类型、联合类型,还是函数类型?
keyof得到的是对象还是键的联合类型?Pick的键是要保留,还是要删除?Omit的键是要保留,还是要删除?Exclude是否正在处理联合类型?ReturnType接收的是不是函数类型?- 类型变化只发生在类型检查阶段,还是会生成运行时代码?
TypeScript 工具类型只参与类型检查,不会生成对应的运行时代码。真正建立体系的方法,是沿着"输入类型 → 中间类型 → 输出类型"逐步推导,而不是孤立背诵名称。