啃完 TS 工具类型我发现:Pick 和 Omit 原来就是一层窗户纸

写后台接口的时候,有个问题一直卡着我:同一个 User 类型,列表页只要 idname,详情页又不想要 email 这种敏感字段,更新接口里所有字段还都得是可选的。以前我的做法就是复制粘贴 interface,改改字段名,一套接口下来能整出三四个长得差不多的类型。说实话越写越恶心,知道 TS 里肯定有更体面的写法,但一直没认真捋。

直到上次项目里类型报错把我逼急了,才抽了半天把 Pick、Omit、Partial、Exclude 这几兄弟从头到尾看了一遍,看完第一个反应是:我之前绕的弯都值了------它们真的不难,就是一开始没人给你把那层纸捅破。

从 Pick 入门:原来就是从类型里"挑字段"

最开始我是从 Pick 下手的,因为它名字最直白。我拿项目里最常见的 User 做了个例子:

typescript 复制代码
interface User {
    id: number;
    name: string;
    age: number;
    email: string;
}

列表页只展示用户 ID 和昵称,我就写了这么一行:

typescript 复制代码
type UserPreview = Pick<User, 'id' | 'name'>;

const u: UserPreview = {
    id: 1,
    name: 'xjl',
};

跑了一下没报错。当时我理解的就是:Pick 是一个"挑菜"的工具------User 是一盘满汉全席,第二个参数就像我的筷子,想夹哪几样就夹哪几样,夹出来的就是一个新的小盘。

说实话这个阶段我甚至没多想 'id' | 'name' 为什么是竖线(联合类型),反正照着写能用就行。现在回头看,这种"先会用再说"的心态反而让我后面卡了很久。

Omit 反着来:我只要说"不想要的"

顺着 Pick 的思路往下看 Omit,当时我以为它是另一个独立的工具,作用就是"剔除"。比如列表接口我不想把邮箱带出去,就写:

typescript 复制代码
// Omit 去掉部分字段
type UserSafe = Omit<User, 'email'>;

const u2: UserSafe = {
    id: 1,
    name: 'xjl',
    age: 18,
};

这个也顺,一个是"挑哪些留下",一个是"说哪些丢掉"。我当时甚至还自负地觉得:这俩不就是正反两面吗,谁还用记啊。

捋到这我突然想通,为啥这俩工具要成对出现------因为项目里真实的场景决定了怎么写舒服。字段少的时候就 Pick,明确挑出来;字段多、只需要去掉一两个的时候就 Omit,省得写一长串 'a' | 'b' | 'c'。就像删文档,删两个字直接划掉就行,删两百字你不如复制一份保留的部分。

跑了个小 demo 把常用的都串了一遍

那天我顺着把 Partial、Record、ReturnType、Exclude 都写了一遍,放在同一个文件里看它们之间的关系。先看 Partial,这玩意儿在更新接口里是真的香:

typescript 复制代码
// 所有字段全部变成可选
type PartialUser = Partial<User>;
// patch 修改,对象属性很多
const u3: PartialUser = {
    name: 'xjl',
    age: 18,
};
const emptyObj: PartialUser = {}; // 全空也合法

说白了就是把 User 里每个字段前面都加了一个 ?。做 PATCH 请求的时候最爽,前端想改哪个字段就传哪个,不用再手动定义一遍"全部可空"的 UpdateUser。

然后是 Record,之前我一直以为它是专门给字典对象用的,直到我把它和后端的错误码表对应起来,一下就顺了:

typescript 复制代码
// json key:value Record<键类型, 值类型>
type Dict = Record<string, number>;
const obj: Dict = { a: 1, b: 2 };

type ErrorMsgMap = Record<number, string>;
// http status code 3xx 跳转 / 4xx 用户错 / 5xx 服务器错
const errorMessage: ErrorMsgMap = {
    400: '请求参数错误',
    401: '未登录',
    403: '没有权限',
    404: '资源不存在',
    500: '服务器错误',
};

function getErrMsg(code: number): string {
    return errorMessage[code] ?? '未知错误';
}

还有 ReturnType,用来拿函数返回值的类型,日常做类型体操的时候会经常碰到:

typescript 复制代码
function fn() {
    return { x: 1, y: 2 };
}
type fnReturn = ReturnType<typeof fn>; // { x: number; y: number }

最后 Exclude,这个我一开始最懵------它跟 Omit 不都是"排除"吗?写了两行对比我才发现根本不是一回事:

typescript 复制代码
// 联合类型
type All = 'id' | 'name' | 'age' | 'email';
type AfterExclude = Exclude<All, 'email'>; // 'id' | 'name' | 'age'

Exclude 是联合类型 层面的剔除,它连对象都不是,就处理一串 'a' | 'b' | 'c'。而 Omit 处理的是整个 interface / 对象类型。一个管"字符串集合",一个管"整个对象",完全是两个层级的操作。

翻 TS 内部实现我愣了,Omit 居然是个"组合件"

看到上面那个分层关系,我就手贱去翻了 TS 内置类型的实现。说实话我本来以为 Omit 是 TS 团队单独写的一套逻辑,结果一看愣了:

typescript 复制代码
// TS 内部 Omit 的等价实现
type MyOmit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

就一行。我盯着这行看了有半分钟,脑子一层层展开:

  1. keyof T,拿到 T 所有键的联合类型。比如 keyof User 就是 'id' | 'name' | 'age' | 'email'
  2. 然后 Exclude<上面的结果, K>,把要剔除的键从联合类型里删掉------比如 K 是 'email',这一步就得到 'id' | 'name' | 'age'
  3. 最后再 Pick<T, 剩下的键>,就是刚才会用的 Pick 嘛。

之前笔记里写的那串 Omit<T, K> 等价于 Pick<T, Exclude<keyof T, K>> 我一直背不住,翻完源码突然就记牢了------因为它不是公式,就是 TS 作者实际写的代码。我在 2.ts 里手动拆了一遍验证:

typescript 复制代码
type UserKeys = keyof User;       // 联合类型
type KeepKeys = Exclude<UserKeys, 'email'>; // 剔除 email
type MyOmitUser = Pick<User, KeepKeys>;     // 再 Pick,结果和 Omit 一模一样

说实话这个瞬间是我当天最有收获的时刻。以前背工具类型就像背单词表,Pick、Omit、Partial 每一个都是独立的新单词;现在突然发现它们其实是有词根的------Exclude 管联合,Pick 管对象,Omit 只是把这俩粘起来的胶水。知道词根以后,真的就不太容易忘。

有几个姿势确实容易写反

这一路写下来我踩了两个最直观的小坑,给你们点一下就够了。

第一个就是 Pick 的第二个参数必须是 keyof T 里面的成员 ,乱写字段名 TS 会直接报错。比如你写 Pick<User, 'phone'> 肯定红,因为 User 里根本没这个键。Omit 其实宽松一点,删掉一个不存在的键有时候不报错,所以有人就喜欢用 Omit 图省事,这点我觉得还是 Pick 更能约束自己。

第二个就是 Exclude 和 Omit 的维度搞混 ,我最开始差点写出 Exclude<User, 'email'> 这种东西------Exclude 只吃联合类型,塞进去一个 interface 它啥也不会干。

Pick 第二参数、Omit 第二参数的类型层级不一样:Pick 要求是 keyof T 的子集;Omit 只是把 K 交给 Exclude 去做联合类型运算,所以写错 key 的时候 Pick 会更早地帮你兜底。

还有个不致命但挺膈应人的:Partial 是全部字段都变可选,包括嵌套对象也是整体变可选但内部属性不递归。有时候业务想做深度可选,你自己得再封装个 DeepPartial,别上来就拿 Partial 硬顶。

收获就仨:少写、会拆、不硬背

啃完这一圈我最大的变化不是"多会了几个工具类型",而是写 TS 的心态变了。

以前是遇到复杂类型就怂,赶紧复制一份 interface 改改字段数。现在我会先停一下:这个场景是"只要少数几个字段"还是"就去掉一两个"?前者 Pick,后者 Omit;是不是做更新?那直接 Partial;是不是键值对映射?那就是 Record。按场景选工具,而不是按记忆生搬硬套。

然后就是源码那一层真的值得看一看。TS 工具类型的内置实现很多都只有一两行,全是用更基础的 keyofinextends 拼出来的,看懂一层就能串起一串。自己手写一个简化版 Omit,比背十道面试题都管用。

适用边界也想提一句:小项目、一两个人写的 demo 真没必要玩这些,直接写具体类型跑得飞快。但一旦是有几十个 interface、前后端类型要对齐、接口分增删改查多种形态的中大型项目,Pick / Omit / Partial 这几个是真的能显著减少重复代码,改字段的时候改一次源头就够了,不用满项目搜同结构的 interface 一个个改。

顺手捋的核心知识点

写到这顺手把核心的点捋了一遍,我自己回头复盘方便,你们要是懒得翻长文也可以直接看这部分。

1. Pick ------ 从对象类型里按字段白名单挑出新类型

  • 核心结论:Pick<T, K> 把对象类型 T 中属于联合类型 K 的字段保留,生成新对象类型。
  • 易错提醒:K 必须是 keyof T 的子集,写了不存在的字段会直接类型报错。

2. Omit ------ 从对象类型里按黑名单剔除字段出新类型

  • 核心结论:Omit<T, K> 把对象类型 T 中属于联合类型 K 的字段去掉,生成新对象类型。
  • 易错提醒:Omit 是对象类型层面的操作,它本身不处理联合类型;处理联合类型要用 Exclude。

3. 实现类:Omit 的底层等价写法(3 步拆解)

  • 核心结论:Omit<T, K> 本质是 Pick<T, Exclude<keyof T, K>>,三步骤:① keyof T 取所有键的联合 → ② Exclude<, K> 在联合类型层面删黑名单 → ③ Pick 再把剩下的键从 T 里挑出来。
  • 关键步骤:先 keyof 降维成联合 → 联合运算 → 再 Pick 升维回对象,这是很多 TS 工具类型的通用套路。
  • 易错提醒:别记成 Exclude<Pick<T, ...>, K> 这种套反的顺序,记住是"先算键再 Pick"。

4. Exclude ------ 联合类型的子集剔除运算

  • 核心结论:Exclude<U, K> 在联合类型 U 里把属于 K 的成员去掉,得到新联合类型。
  • 易错提醒:Exclude 只吃联合类型,往里面塞 interface 得不到你想要的"去掉字段"效果,那是 Omit 的活。

5. Partial ------ 把对象类型的所有字段一次性变可选

  • 核心结论:Partial<T> 等价于给 T 中每个字段前加 ?,PATCH/部分更新场景最常用。
  • 易错提醒:Partial 是浅层可选,嵌套对象内部不会递归变可选;需要深度可选要自己写 DeepPartial。

6. Record ------ 快速定义键值映射的字典类型

  • 核心结论:Record<K, V> 生成"键类型为 K、值类型为 V"的对象类型,比如错误码表、枚举映射。
  • 易错提醒:键类型 K 一般用 string | number | symbol,别把整个 interface 塞进来当键。

7. ReturnType ------ 抽取函数返回值的类型

  • 核心结论:ReturnType<typeof fn> 可以直接从函数 fn 拿到它返回值的对象类型,避免手写真值重复。
  • 易错提醒:参数里必须是 typeof fn 传函数的类型,不能直接写函数名 fn,否则是值不是类型。

我目前理解到的程度大概就是这些,有补充的可以评论区唠唠。

相关推荐
风月说与山鬼3 小时前
三、大括号语法
前端·react.js
kyriewen3 小时前
我把最常踩的8个CORS跨域报错整理了一遍——第8个去年还不存在
前端·javascript
Csvn3 小时前
🕰️ 闭包 + setTimeout 的 5 个经典陷阱:为什么定时器看到的永远不是最新的值?
前端
lilian2334 小时前
Harmony os 技术实战|拼豆制图27:用单字符编码承载 50 张 70×70 图纸
前端·数据库·华为·harmonyos
JaydenAI4 小时前
[TypeScript学习笔记-07]根据现有类型创建新类型的N种方式
typescript
CarIise4 小时前
CSS选择器与样式关联
前端·css·tensorflow
里欧跑得慢5 小时前
CSS 模块化架构的演进:BEM、CSS Modules 到 CSS-in-JS 的反思
前端·css·flutter·web·css-in-js
IT_陈寒6 小时前
Vue的computed属性把我坑惨了,原来我一直用错姿势
前端·人工智能·后端