TypeScript 工具类型七件套:从裁剪到组合,掌握类型推导的核心武器
写 TypeScript 到一定阶段,会频繁遇到一类需求:从一个已有的对象类型,推导出另一个相关的类型——"挑几个字段出来""去掉某个敏感字段""所有字段都变可选"。这些需求如果每次都手写一遍,既啰嗦又容易漏。TypeScript 内置了一批**工具类型(Utility Types)**专门干这个,本文就把最常用的七个一次性讲清,并深入拆解 Omit 的底层实现,让你不仅会用,还能自己拼。
在复杂的、大型的项目里,类型消费比较大,用好这些工具类型能大幅减少重复的类型定义。而其中最值得深挖的,是
Omit那句"等价于Pick<T, Exclude<keyof T, K>>"——它背后藏着一套可复用的类型推导思路。
一、起点:一个 User 接口
所有的工具类型都围绕同一个源类型展开。我们先定义一个最朴素的 User:
interface User {
id: number
name: string
age: number
email: string
}
后面的每一个类型,都是从 User 这个"原材料"里裁剪、变换出来的。理解了"以 User 为源"这个前提,七个工具类型就有了统一的参照系。
二、Pick:挑几个字段出来
第一个需求:从一个类型中挑选一些你需要的字段,形成新的类型。对应的工具类型是 Pick:
// Pick 从类型中挑选一些字段,形成新的类型
type UserPreview = Pick<User, 'id' | 'name'>
const u: UserPreview = {
id: 1,
name: '张三',
// age: 18, // 报错:UserPreview 里没有 age
}
Pick<T, K> 的签名是 Pick<T, 选取类型的联合字符串>——第一个参数是源类型,第二个参数是你想保留的字段名组成的联合类型。Pick<User, 'id' | 'name'> 得到的新类型只含 id 和 name,多写一个 age 编译器就会拦下来。
典型场景:列表预览。列表页只想展示几个字段,用 Pick 裁出一个"轻量版"类型,既约束了传入的数据形状,又不用维护一份重复的字段清单。
三、Omit:排除某个字段
和 Pick 正好相反的需求是排除一些字段——比如把敏感字段 email 去掉再对外输出:
// Omit 从类型中排除一些字段,形成新的类型
type UserSafe = Omit<User, 'email'>
const uSafe: UserSafe = {
id: 2,
name: '廖昊',
age: 19,
// email 已经不存在了
}
Omit<T, K> 的签名是 Omit<T, 排除类型的联合字符串>:第二个参数是你要删掉的字段。Omit<User, 'email'> 得到的 UserSafe 保留了除 email 外的全部字段。
Pick 和 Omit 是一对镜像:Pick 白名单式地"留",Omit 黑名单式地"删"。当要删的字段少时用 Omit 更省事(一个 'email' 就够),当要留的字段少时用 Pick 更清晰。
四、Partial:让所有字段变可选
第三个常见需求是 "patch" 修改——更新对象时只传要改的字段,没传的保持原值:
// Partial 所有字段全部可选
type PartialUser = Partial<User>
// patch 修改,对象属性很多
const patchUser: PartialUser = {
name: '廖昊',
age: 19,
}
Partial<T> 把 User 的每个字段都变成可选,所以 patchUser 可以只写 name 和 age,不写 id 和 email。
落地场景:对象属性很多的时候做部分更新,配合后端 PATCH 语义(只改一部分)非常契合。比如更新用户信息,前端只需构造一个包含待改字段的对象,类型上使用 Partial<User> 就天然允许缺省。
五、Record:构造一个键值字典
Record 和前面几个不同,它不是"裁剪",而是构造——根据键类型和值类型,造出一个字典:
// JSON key:value Record<键类型, 值类型>
type Dict = Record<string, number>
const obj: Dict = { a: 1, b: 2 }
Record<K, V> 的两个参数分别是键的类型和值的类型。Record<string, number> 就是"字符串键、数字值"的字典。
一个特别实用的例子——HTTP 状态码 → 错误信息的映射:
type ErrorMsgMap = Record<number, string>
const errorMsgMap: ErrorMsgMap = {
400: '请求参数错误',
401: '未登录,需要先登录',
403: '权限不足,禁止访问',
404: '资源不存在',
500: '服务器内部错误',
}
function getErrorMsg(code: number) {
return errorMsgMap[code] ?? '未知错误'
}
这里 Record<number, string> 把"状态码(number)→ 提示文案(string)"这个映射关系固化成类型,再配合 ?? 兜底一个"未知错误"。Record 让"键值查找表"有了类型保障,getErrorMsg 里查不存在的 code 也不会返回 undefined。
六、ReturnType 与 Exclude:两种"取出来"
还有两个工具类型,分别从"函数"和"联合类型"里取东西。
6.1 ReturnType:取函数的返回值类型
function fn() {
return { x: 1, y: 2 }
}
// ReturnType<函数类型> 函数返回值类型
type FnReturn = ReturnType<typeof fn> // { x: number, y: number }
注意写法是 ReturnType<typeof fn>——ReturnType 要的是函数类型,而 fn 是函数值,所以要先用 typeof fn 拿到它的类型再传入。好处是:如果 fn 的返回值结构变了,FnReturn 会自动跟着变,不用手动同步。
6.2 Exclude:从联合类型里删除指定成员
type All = "id" | "name" | "age" | "email"
// Exclude<联合类型, 排除的类型> 排除联合类型中的指定类型
type AfterExclude = Exclude<All, "email"> // "id" | "name" | "age"
Exclude<联合类型, 排除的类型> 从联合类型里删掉指定成员。
一个重要辨析:Omit 处理对象接口,Exclude 处理联合类型。即:Omit 操作的是对象类型(去掉对象的字段),Exclude 操作的是联合类型(去掉联合里的成员)。两者的对象不一样,别混用。
七、核心:Omit 的三步底层实现
笔记最精彩的地方,是把 Omit 这个"黑盒"拆开了。开头那句:
Omit<T, K>等价于Pick<T, Exclude<keyof T, K>>,怎么理解?
答案藏在三个中间步骤里:
// ① keyof 获取对象的所有属性名
type UserKeys = keyof User // "id" | "name" | "age" | "email"
// ② 从联合类型里排除 'email'
type KeepKeys = Exclude<UserKeys, 'email'> // "id" | "name" | "age"
// ③ 从原类型中 Pick 出需要保留的键
type MyOmitUser = Pick<User, KeepKeys> // { id: number, name: string, age: number }
拆解成三步,Omit 的底层逻辑就一目了然:
keyof T拿到所有键的联合类型——keyof User得到"id" | "name" | "age" | "email"。Exclude把要剔除的K删掉,剩下需要保留的键——Exclude<UserKeys, 'email'>得到"id" | "name" | "age"。- 再用
Pick把剩下的键从T中提取出来——Pick<User, KeepKeys>得到{ id, name, age }。
三步走完,就得到了 Omit<T, K>。这就是 TypeScript 内部 Omit 类型的实现原理:Omit 本身不是原子操作,而是 Pick、Exclude、keyof 三个工具类型组合出来的。
这个拆解的价值不只是"看懂 Omit",更在于它演示了一种思考方式——用几个简单工具类型拼出更复杂的类型。遇到没有现成工具类型能满足的需求时,学着像这样"拆成 keyof → Exclude → Pick 的组合"自己拼一个,是进阶 TypeScript 的核心能力。
八、七件套全景
最后把七个工具类型收进一张表,方便对照记忆:
| 工具类型 | 签名 | 作用 | 操作对象 |
|---|---|---|---|
Pick | Pick<T, K> | 挑选字段,形成新类型 | 对象类型 |
Omit | Omit<T, K> | 排除字段,形成新类型 | 对象类型 |
Partial | Partial<T> | 所有字段变可选 | 对象类型 |
Record | Record<K, V> | 构造键值字典 | 键 + 值 |
ReturnType | ReturnType<typeof fn> | 取函数返回值类型 | 函数类型 |
Exclude | Exclude<U, K> | 从联合类型里排除成员 | 联合类型 |
keyof | keyof T | 取对象所有键的联合类型 | 对象类型 |
记忆要点两条:Omit 操作对象接口、Exclude 操作联合类型;Omit = Pick + Exclude + keyof 的组合。抓住了这两条,七件套的骨架就立住了。
九、进阶组合:自己拼出更多工具类型
理解了 Omit 的组合原理,你可以自己拼出更多的工具类型。例如:
// 让指定字段变为必选(与 Partial 相反)
type Required<T> = { [P in keyof T]-?: T[P] }
// 提取对象中值为特定类型的字段
type PickByType<T, U> = { [P in keyof T as T[P] extends U ? P : never]: T[P] }
// 从类型中排除值为特定类型的字段
type OmitByType<T, U> = { [P in keyof T as T[P] extends U ? never : P]: T[P] }
这些都可以基于 keyof、映射类型、条件类型组合出来。TypeScript 的类型系统是图灵完备的,组合思维比记住每一个工具类型更重要。
十、面试高频问题与答题框架
Q1:Pick 和 Omit 有什么区别?分别用在什么场景?
回答框架:
Pick<T, K> 从类型里挑选一些字段形成新类型(白名单式"留"),Omit<T, K> 从类型里排除一些字段形成新类型(黑名单式"删")。要删的字段少(如去掉 email)用 Omit 更省事,要留的字段少(如列表只展示 id/name)用 Pick 更清晰。二者是一对镜像。
Q2:Omit<T, K> 等价于 Pick<T, Exclude<keyof T, K>>,怎么理解?
回答框架:
拆成三步:① keyof T 拿到所有键的联合类型;② Exclude 把要剔除的 K 删掉,剩下需要保留的键;③ 再用 Pick 把剩下的键从 T 中提取出来,最后得到 Omit<T, K>。所以 Omit 不是原子操作,而是 Pick + Exclude + keyof 三个工具类型组合出来的,这正是 TypeScript 内部 Omit 的实现原理。
Q3:Partial 的典型使用场景是什么?
回答框架:
部分更新(patch)。更新对象时只传要改的字段、没传的保持原值,比如 const patchUser: Partial<User> = { name: '廖昊', age: 19 } 可以只写两个字段。Partial<T> 把每个字段都变可选,对象属性很多时特别有用,与后端 PATCH 的"只改一部分"语义契合。
Q4:Record 是什么?Exclude 和 Omit 的对象有什么不同?
回答框架:
Record<K, V> 根据键类型和值类型构造一个键值字典,如 Record<number, string> 表示"数字键、字符串值",常用于 HTTP 状态码 → 错误信息的映射表。辨析上:Omit 操作的是对象接口(去掉对象的字段),Exclude 操作的是联合类型(去掉联合里的成员),两者对象不同,别混用。
Q5:ReturnType 为什么要写 ReturnType<typeof fn> 而不是 ReturnType<fn>?
回答框架:
ReturnType 要的是函数类型,而 fn 是函数值,所以要先用 typeof fn 拿到它的类型再传入。这样如果 fn 的返回值结构变了,ReturnType 推导出的类型会自动跟着变,不用手动同步。
Q6:能否自己组合出新的工具类型?举个例子。
回答框架:
可以。TypeScript 的类型系统支持映射类型和条件类型,我们可以基于这些基础能力组合出新工具。比如提取对象中值为特定类型的字段:
type PickByType<T, U> = { [P in keyof T as T[P] extends U ? P : never]: T[P] }
这种组合思维比记住每一个现成工具类型更重要。
结语:工具类型是"类型推导的组合拳"
本文没有引入新的语言特性,却把一个进阶 TypeScript 开发者绕不开的技能点打透了——用内置工具类型,从已有类型推导出新类型。回看整条线,其实是两层认知:
- 会用:
Pick/Omit/Partial/Record/ReturnType/Exclude/keyof各自解决一类裁剪/构造需求。 - 会拆:
Omit = Pick + Exclude + keyof——用简单工具类型组合出复杂类型。
落到记忆上就两条:Omit 管对象、Exclude 管联合;Omit 是 Pick + Exclude + keyof 的三步组合。
动手前,拿这份清单自检:
- 能否说清
Pick<T, K>(挑选)和Omit<T, K>(排除)是一对镜像? - 能否把
Omit = Pick<T, Exclude<keyof T, K>>拆成keyof → Exclude → Pick三步讲清楚? - 能否说出
Partial<T>用于patch部分更新的场景? - 能否用
Record<number, string>构造一个状态码 → 错误信息的映射表? - 能否解释
ReturnType<typeof fn>里为什么要加typeof? - 能否分清
Omit操作对象接口、Exclude操作联合类型? - 能否说出
keyof T拿到的是"所有键的联合类型"?
掌握这七个工具类型,尤其是 Omit 背后的组合思路,就拿到了类型推导的第一套组合拳——裁剪用 Pick/Omit、可选用 Partial、字典用 Record、取值用 ReturnType/Exclude/keyof,复杂需求自己拼。这不仅是记住几个 API,更是理解 TypeScript 类型系统组合之美的第一步。