"接口字段要挑几个、删几个、全部改可选......我一开始全是手写新 interface,累到怀疑人生。直到学会了这几个工具类型,原来 TS 早就把活给你准备好了。"
前言
刚学 TypeScript 那会儿,我有个 User 接口到处用:列表页要展示 id + name,编辑页要提交 id + name + age,而登录后返回的接口数据又不能把 email 暴露给前端。
于是我很老实地写了三四个长得几乎一样的 interface,字段删删减减,改一个地方要同步改一堆。
后来才知道,这些"从已有的类型里挑挑拣拣、生成一个新类型"的操作,TS 官方早就内置了一套工具类型(Utility Types),一句话就能搞定。这篇文章我把我踩过的坑和最终搞懂的思路整理出来,每个类型都配可运行代码 + 大白话 + 对比表。
你将会收获:
- 🎯 搞懂 8 个高频工具类型:
Pick、Omit、Partial、Exclude、keyof、ReturnType、Record - 🔑 彻底理解那句绕口的
Omit<T, K>等价于Pick<T, Exclude<keyof T, K>> - 🔧 每个类型配完整可运行代码,粘贴即用
- 📊 HTTP 状态码实战 + 面试速查表 + 口诀,考前扫一眼就够
技术栈:TypeScript。
📌 阅读提示 :全文围绕一个
User接口展开,先记住这个"主角",后面每个类型都是对它的改造。
目录
- 一、工具类型到底解决什么问题?
- 二、keyof:拿到所有键的联合类型(地基)
- 三、Pick:从类型里挑字段
- 四、Omit:从类型里删字段
- [五、核心:Omit 其实等于 Pick + Exclude](#五、核心:Omit 其实等于 Pick + Exclude "#%E4%BA%94%E6%A0%B8%E5%BF%83omit-%E5%85%B6%E5%AE%9E%E7%AD%89%E4%BA%8E-pick--exclude")
- 六、Partial:所有字段变成可选
- 七、Record:键值对的字典映射
- 八、ReturnType:拿到函数返回值类型
- 九、Exclude:联合类型的差集
- [十、终极速查表 + 面试高频 N 问](#十、终极速查表 + 面试高频 N 问 "#%E5%8D%81%E7%BB%88%E6%9E%81%E9%80%9F%E6%9F%A5%E8%A1%A8--%E9%9D%A2%E8%AF%95%E9%AB%98%E9%A2%91-n-%E9%97%AE")
一、工具类型到底解决什么问题?
先用大白话讲清楚痛点,再讲它是什么。
假设我们有一个用户接口:
ts
interface User {
id: number;
name: string;
age: number;
email: string;
}
现在有个需求:列表页只需要 id 和 name,其他字段都不想要。
"笨办法"是重新写一个 interface:
ts
// ❌ 笨办法:手写一个精简版,字段和 User 是复制粘贴的关系
interface UserPreview {
id: number;
name: string;
}
字段一多、接口一变,你就得人肉同步好几份,改漏一个就出 bug。
工具类型要解决的,就是这个"从已有类型派生新类型"的活儿:
ts
// ✅ 一句话:从 User 里挑出 id 和 name,生成新类型
type UserPreview = Pick<User, 'id' | 'name'>;
大白话解释
| 概念 | 一句人话 | 类比 |
|--------------------|------------------|-------------------------|------------|
| 工具类型 | 拿来一个旧类型,加工出一个新类型 | 一个"类型加工厂" |
| `Pick<User, 'id' | 'name'>` | 从 User 里挑出 id、name 两个字段 | 菜单点菜:勾选几个菜 |
💡 核心思想 :工具类型不会凭空造类型,它一定是基于一个已有类型做变换 。这个"已有类型"就是下文反复出现的
User。
下面按"地基 → 正向挑 → 反向删 → 原理 → 其他几个"的顺序,逐个击破。
二、keyof:拿到所有键的联合类型(地基)
学其他工具类型之前,必须先搞懂 keyof------它是后面所有人的地基。
大白话
keyof T 会把类型 T 的所有键名 抽出来,拼成一个联合类型 (| 连接的字符串)。
ts
interface User {
id: number;
name: string;
age: number;
email: string;
}
type UserKeys = keyof User;
// UserKeys = 'id' | 'name' | 'age' | 'email'
keyof User 翻译成人话就是:"User 身上所有字段名的集合"。
类比
keyof 像一份员工花名册------它不关心每个字段的值是什么类型,只把"名字"列出来。
| 概念 | 输出 | 类比 |
|---|---|---|
keyof User |
`'id' | 'name' |
💡 一句话记住 :
keyof T= 拿到 T 的所有键,拼成一个|联合类型。后面Pick、Omit、Exclude全靠它。
三、Pick:从类型里挑字段
Pick<T, K> 从类型 T 里挑出 K 指定的那些字段,组成新类型。K 就是要挑的键的联合字符串。
大白话
Pick = 点菜 。菜单就是 User,你勾选哪几个字段,最后端上桌的就只有那几样。
ts
interface User {
id: number;
name: string;
age: number;
email: string;
}
// 从 User 里挑出 id 和 name,形成一个"精简版用户"类型
type UserPreview = Pick<User, 'id' | 'name'>;
const u: UserPreview = {
id: 1,
name: '敬峰',
};
// ✅ 只有 id、name 两个字段,写多了报错,写少了也报错
逐行讲解
| 代码 | 含义 |
|---|---|
| `Pick<User, 'id' | 'name'>` |
type UserPreview = ... |
给新类型起个名字 |
const u: UserPreview = {...} |
变量 u 只能有 id、name |
为什么需要它?------ 复杂项目的"区分度"
🎯 实战意义 :大型项目里类型消费特别多------同一个
User,列表页、详情页、编辑页、接口返回各自需要的字段都不一样。用Pick就能按场景派生出精确的类型,该暴露几个字段就暴露几个 ,类型边界清晰,别人一看UserPreview就知道这是"只给看 id 和 name 的轻量版"。
四、Omit:从类型里删字段
Omit<T, K> 和 Pick 正好相反:从类型 T 里删掉 K 指定的字段,留下其余的全部。
大白话
Omit = 拉黑。你说"不要 email",剩下的全给你。
ts
interface User {
id: number;
name: string;
age: number;
email: string;
}
// 去掉 email 字段,剩下的 id、name、age 组成新类型
type UserSafe = Omit<User, 'email'>;
const safeUser: UserSafe = {
id: 1,
name: '敬峰',
age: 20,
};
// ✅ 没有 email 字段了,适合"不能把邮箱暴露给前端"的场景
Pick vs Omit 怎么选?
| 场景 | 用哪个 | 例子 |
|---|---|---|
| 要的字段少(挑少数几个) | Pick |
列表页只要 id、name |
| 不要的字段少(删少数几个) | Omit |
只不想要 email |
💡 经验 :要留的多就
Omit,要留的少就Pick,哪个写起来字段少用哪个。
五、核心:Omit 其实等于 Pick + Exclude
这是全文的重点,也是面试最爱追问的一句绕口令:
Omit<T, K>等价于Pick<T, Exclude<keyof T, K>>
我第一次看到这句话的时候是懵的,拆开一步步走就通了:
分三步走
第一步:keyof T 拿到所有键的联合类型
ts
type UserKeys = keyof User;
// 'id' | 'name' | 'age' | 'email'
第二步:Exclude 把要剔除的 K 键删掉,剩下需要保留的键
ts
type KeepKeys = Exclude<UserKeys, 'email'>;
// 'id' | 'name' | 'age' ------ email 被从联合类型里删掉了
第三步:再用 Pick 把剩下的键从类型 T 里挑出来,就实现了 Omit 的效果
ts
type MyOmitUser = Pick<User, KeepKeys>;
// 等价于 Omit<User, 'email'>
一张图看懂数据流
bash
keyof User Exclude<..., 'email'> Pick<User, 剩下的键>
┌──────────────┐ 删掉 email ┌──────────────┐ 挑出来 ┌────────────────┐
│ id name │ ─────────────→ │ id name │ ─────────→ │ 只含 id name age │
│ age email │ │ age │ │ 的 User 新类型 │
└──────────────┘ └──────────────┘ └────────────────┘
① 拿键 ② 剔除 ③ 挑选
TS 内部 Omit 的等价实现
TypeScript 源码里,Omit 就是这么定义的(lib.d.ts 里能找到):
ts
// Omit 的官方实现:先把 K 从 keyof T 里排除,再用 Pick 挑回来
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
也就是说,Omit 并不是一个"原生"能力,而是 Pick + Exclude 这两个更底层能力的组合------它就是一层语法糖,图省事而已。
⚠️ 面试考点 :"Omit 的底层原理是什么?" ------ 答:
Omit<T, K>等于Pick<T, Exclude<keyof T, K>>,先keyof拿键 →Exclude剔除 K →Pick挑剩余,本质是Pick和Exclude的组合语法糖。
六、Partial:所有字段变成可选
Partial<T> 把类型 T 的所有字段全部变成可选 (每个字段后自动加 ?)。
大白话
Partial = 把必答的填空题,全部改成选做题------你可以一个都不填。
ts
interface User {
id: number;
name: string;
age: number;
email: string;
}
// 所有字段都变成可选了
type PartialUser = Partial<User>;
// patch 修改场景:对象属性很多,只想改其中一两个
const patchUser: PartialUser = {
id: 1,
name: '敬峰',
age: 20,
};
// 甚至可以传空对象(所有字段都可选)
const emptyObj: PartialUser = {};
实战意义:patch 更新
🎯 典型场景 :做一个"修改用户信息"的接口,前端可能只改邮箱、只改年龄,也可能全改。用
Partial<User>当参数类型,就允许只传部分字段,不用每改一个字段就重新写一个接口。
ts
// 只改邮箱,其他字段都不用传
function updateUser(id: number, patch: Partial<User>) {
// ... 把 patch 里有的字段更新到数据库
}
updateUser(1, { email: 'new@example.com' }); // ✅ 只传 email
对比
| 写法 | 字段是否必填 | 场景 |
|---|---|---|
User |
全部必填 | 创建新用户 |
Partial<User> |
全部可选 | 部分更新(patch) |
七、Record:键值对的字典映射
Record<K, V> 快速声明一个"键值对"类型:键的类型是 K,值的类型是 V。
大白话
Record = 电话簿 / 字典 。Record<键类型, 值类型> 就是"所有 key 是某类型、所有 value 是某类型"的一张表。
ts
// 一个字符串到数字的字典:key 是 string,value 是 number
type Dict = Record<string, number>;
const obj: Dict = { a: 1, b: 2 };
// ✅ 键是字符串,值是数字
// 数字到字符串的映射:key 是 number,value 是 string
type ErrorMsgMap = Record<number, string>;
实战:HTTP 状态码 → 错误提示
这是 Record 最经典的用法,把状态码映射成一句人话:
ts
type ErrorMsgMap = Record<number, string>;
// HTTP 状态码的语义:
// 1xx 执行中 / 2xx 成功 / 3xx 要跳转 / 4xx 用户错误 / 5xx 服务端错误
const errorMessage: ErrorMsgMap = {
400: '请求参数错误',
401: '未登录,请重新登录',
403: '权限不足,禁止访问',
404: '资源找不到',
500: '服务器内部错误',
};
function getErrMsg(code: number) {
// ?? 是空值合并:查得到就返回对应文案,查不到兜底"未知错误"
return errorMessage[code] ?? '未知错误';
}
getErrMsg(404); // '资源找不到'
getErrMsg(999); // '未知错误'
对比
| 写法 | 含义 | 类比 |
|---|---|---|
Record<string, number> |
key 是 string,value 是 number | 通讯录:名字 → 电话 |
Record<number, string> |
key 是 number,value 是 string | 状态码 → 错误文案 |
💡 一句话记住 :
Record<K, V>就是"键类型 + 值类型",键值对字典专用。
八、ReturnType:拿到函数返回值类型
ReturnType<T> 拿到一个函数的返回类型 。注意传的是 typeof 函数名,不是函数本身。
大白话
ReturnType = 让函数"自己交代"它返回了什么,不用你人肉去看。
ts
function fn() {
return { x: 1, y: 2 };
}
// 拿到 fn 的返回类型 = { x: number, y: number }
type FnReturn = ReturnType<typeof fn>;
// FnReturn 现在就是个普通对象类型,可以这样用
const result: FnReturn = { x: 10, y: 20 }; // ✅
为什么传 typeof fn?
ReturnType 要的是函数的类型 ,而 fn 是函数的值,所以要用 typeof fn 把"值"转成"类型"。这是个小坑,很多人第一次写漏 typeof 直接报错:
ts
// ❌ 错误:ReturnType<fn> ------ fn 是值不是类型,报错
// type Wrong = ReturnType<fn>;
// ✅ 正确:用 typeof 拿到函数的类型
type Right = ReturnType<typeof fn>;
💡 一句话记住 :
ReturnType<typeof fn>,typeof千万别漏。
九、Exclude:联合类型的差集
Exclude<T, U> 从联合类型 T 里剔除 掉 U 里出现的成员。
大白话
Exclude = 从名单里划掉 几个人。注意它处理的是联合类型,不是对象。
ts
type All = 'id' | 'name' | 'age' | 'email';
type After = Exclude<All, 'email'>;
// After = 'id' | 'name' | 'age' ------ email 被划掉了
Exclude 和 Omit 别搞混(高频考点)
⚠️ 面试考点 :
Exclude和Omit都是"剔除",但作用对象完全不同。
| 维度 | Exclude |
Omit |
|---|---|---|
| 作用对象 | 联合类型 (`'a' | 'b' |
| 剔除什么 | 联合类型里的某个成员 | 对象里的某个字段 |
| 例子 | `Exclude<'a' | 'b' |
| 类比 | 名单里划掉名字 | 删掉对象的一个属性 |
💡 一句话记住 :
Exclude处理联合类型 ,Omit处理对象接口。二者一个删"成员",一个删"字段"。
十、终极速查表 + 面试高频 N 问
工具类型速查表
| 工具类型 | 语法 | 作用 | 一句话 |
|---|---|---|---|
keyof |
keyof T |
拿所有键的联合类型 | 花名册 |
Pick |
Pick<T, K> |
挑出 K 指定的字段 | 点菜 |
Omit |
Omit<T, K> |
删掉 K 指定的字段 | 拉黑 |
Partial |
Partial<T> |
所有字段变可选 | 选做题 |
Record |
Record<K, V> |
键值对字典 | 电话簿 |
ReturnType |
ReturnType<typeof fn> |
拿函数返回类型 | 让它自己交代 |
Exclude |
Exclude<T, U> |
联合类型差集 | 名单划名字 |
底层实现速查
这些工具类型其实都是 TS 官方"拼"出来的,看懂它们能帮你理解整个类型系统:
| 工具类型 | 底层实现 |
|---|---|
Pick<T, K> |
{ [P in K]: T[P] } |
Omit<T, K> |
Pick<T, Exclude<keyof T, K>> |
Partial<T> |
{ [P in keyof T]?: T[P] } |
Exclude<T, U> |
T extends U ? never : T |
Record<K, V> |
{ [P in K]: V } |
📌 里面的
[P in K]是映射类型语法,属于更进阶的一块,感兴趣可以先把这张表存着,后面单独开一篇讲。
面试高频 N 问
- Omit 和 Exclude 有什么区别?
Omit 作用于对象接口 ,删除的是字段;Exclude 作用于联合类型 ,删除的是成员。Omit<User, 'email'> 删掉 User 的 email 字段,Exclude<'a'|'b'|'c', 'b'> 从联合类型里划掉 'b'。
2. Omit 的底层原理是什么?
Omit<T, K> 等价于 Pick<T, Exclude<keyof T, K>>:先 keyof 拿键 → Exclude 剔除 K → Pick 挑剩余。它是 Pick + Exclude 的组合语法糖。
3. Partial 有什么用?
把所有字段变可选,常用于"部分更新(patch)"场景------允许只传需要修改的字段,而不是必须传全量。
4. ReturnType 为什么要写 typeof?
ReturnType 需要的是函数的类型 ,而函数名是值 ,typeof fn 把值转成类型后才能传进去。
5. Record 一般用在哪?
键值对字典映射,最典型的是 HTTP 状态码 → 错误文案、配置项映射等"用一个 key 查一个 value"的场景。
总结
一句话串起全文:
keyof拿键,Pick挑字段,Omit删字段,Partial变可选,Record做字典,ReturnType取返回,Exclude删联合成员------而Omit本质就是Pick+Exclude的语法糖。
核心代码骨架(5 行记住全部):
ts
interface User { id: number; name: string; age: number; email: string; }
type A = Pick<User, 'id' | 'name'>; // 挑字段
type B = Omit<User, 'email'>; // 删字段 = Pick + Exclude
type C = Partial<User>; // 全可选
type D = Record<number, string>; // 字典
type E = Exclude<keyof User, 'email'>; // 联合类型差集
结尾
这套工具类型刚接触可能觉得"不如手写来得直接",但一旦项目里类型多起来,你就知道它们能省多少复制粘贴。希望这篇文章帮你把这 8 个类型一次捋清楚。
源码在 hjf-ai/interview/ts/pick_partial/ 目录下。有问题欢迎在评论区交流,如果觉得有用,求个点赞收藏 🔥
写于 2026 年 8 月 · TypeScript 面试备战