别只会用 Pick/Omit/Partial!从拼多多一道面试题搞懂 TS 全部内置工具类型

44 阅读6分钟

别只会用 Pick/Omit/Partial!从拼多多一道面试题搞懂 TS 全部内置工具类型

面试官一句"手写一个 Omit",其实是想看你有没有掌握工具类型的组合思维。本文用一道真实面试题,把 TS 内置工具类型一次讲透。

前言

拼多多前端一面的题目很朴素:"手写 Omit<T, K> 的实现"

这道题看着简单,却能精准筛选出两种人:只会背 API 的,和真正理解工具类型底层组合的。

  • 前者:熟练写出 Omit<User, 'email'>
  • 后者:能写出 Pick<User, Exclude<keyof User, "email">>,并讲清每一步发生了什么

本文就沿着这道题,把 PickOmitPartialRecordReturnTypeExcludekeyof 这几个 TS 内置工具类型一次讲透,每个都配真实的业务场景。

  • 适合谁:准备前端面试、想系统搞懂 TS 类型编程的开发者
  • 读完收获:看懂工具类型怎么组合 + 面试手写不再慌 + 业务里用对场景
  • 文末附有完整可验证代码 🧪

项目概览

题目围绕一个 User 接口展开,用各种工具类型派生新类型:

interface User {
    id: number;
    name: string;
    age: number;
    email: string;
}

用到的工具类型全家福:

工具类型作用本 Demo 场景
keyof拿到类型的所有键(联合类型)工具类型的地基
Pick<T, K>挑选部分字段列表页预览 UserPreview
Omit<T, K>剔除部分字段安全版 UserSafe
Partial<T>所有字段变可选更新接口 patch 请求
Record<K, V>构造键值映射HTTP 错误码表
ReturnType<T>取函数返回类型推导函数返回值
Exclude<T, U>从联合类型中剔除Omit 手写的零件

核心知识点

知识点 1:keyof —— 工具类型的"地基"

keyof 不是工具类型,却是一切工具类型的基石。它的作用一句话:把类型的所有键,变成一个联合类型(union)

type UserKeys = keyof User;
// 等价于 type UserKeys = "id" | "name" | "age" | "email";
代码作用
keyof User取出 User 的 4 个键名,拼成联合类型
"id" | "name" | ...联合类型,表示"这 4 个值之一"

为什么这么重要?因为所有工具类型都要先知道类型上有哪些键,才能决定保留还是剔除。keyof 就像一把万能钥匙,先把类型这个"钥匙柜"打开。

知识点 2:Pick —— 从类型里"挑"字段

Pick<T, K> 从类型 T 中,挑选出 K 指定的字段,形成新类型。像点菜——菜单上 4 个菜,你只挑 2 个上桌:

type UserPreview = Pick<User, 'id' | 'name'>;

const u: UserPreview = {
    id: 1,
    name: 'dcx',   // ✅ 只有 id 和 name
    // age: 18,    // ❌ 多了会报错
};
代码作用
Pick<User, 'id' | 'name'>从 User 里只留 idname 两个字段
const u: UserPreview赋值时,多一个字段、少一个字段都会类型报错

业务场景:列表页往往不需要 email 这种敏感字段,用 Pick 直接从原类型"裁剪"出一个预览版,字段变了原类型自动同步,不用写两遍接口类型。

知识点 3:Omit —— 剔除字段 + 面试手写核心

Omit<T, K> 和 Pick 相反,去掉 K 指定的字段。最关键的来了——TS 官方文档明确写道:

Omit<T, K> = Pick<T, Exclude<keyof T, K>>

也就是说,Omit 的底层是用三个"零件"组合出来的。面试时手写 2.ts 的答案就是这 3 步:

type UserKeys = keyof User;               // ① "id"|"name"|"age"|"email"
type KeepKeys = Exclude<UserKeys, "email">; // ② 剔除 email → "id"|"name"|"age"
type MyOmitUser = Pick<User, KeepKeys>;   // ③ 挑选剩下的 → 去掉 email 的 User
步骤代码发生了什么
keyof User打开钥匙柜,拿到 4 把钥匙:"id"|"name"|"age"|"email"
Exclude<..., "email">"email" 这把钥匙扔掉,剩 3 把
Pick<User, KeepKeys>拿着剩下的 3 把钥匙,从 User 里挑出 3 个字段

这就是面试官真正想考的东西——组合思维。 工具类型不是 7 个孤立的 API,而是可以互相拼装的"积木":keyof 开锁 → Exclude 丢钥匙 → Pick 捡钥匙。能把 Omit 拆成这三步,说明你真的理解了类型编程,而不是死记硬背。

应用场景拆解

场景 1:UserPreview —— 列表页隐藏敏感字段

接口给整个 User,前端只想展示 id 和 name:

type UserPreview = Pick<User, 'id' | 'name'>;

直接 Pick 出预览版,不拷贝、不重复定义,源类型变了这里自动跟着变。

场景 2:UserSafe —— 剔除 email

返回给前端的对象不能带 email(隐私),用 Omit

type UserSafe = Omit<User, 'email'>;
const SafeUser: UserSafe = { id: 1, name: 'dcx', age: 18 };

OmitPick 一个"删"一个"留",是一对互补操作。

场景 3:Partial —— 更新接口的 patch 请求

改用户资料时,可能只改一个字段(比如只改 age),全部字段都填又太麻烦。Partial<User>所有字段都可选

type PartialUser = Partial<User>;
const patchUser: PartialUser = { id: 1, name: 'dcx' };  // ✅ 只改需要的
const emptyObj: PartialUser = {};                        // ✅ 甚至可以空

这就是 PATCH 接口(部分更新)的天然类型,也是 Partial 名字的由来:部分(partial)更新。

场景 4:Record —— 键值映射,HTTP 错误码表

Record<K, V> 构造"键的类型为 K、值的类型为 V"的对象。最适合做字典表——比如 HTTP 状态码 → 错误提示:

type ErrorMsgMap = Record<number, string>;
const errorMessage: ErrorMsgMap = {
    400: "请求参数错误",
    401: "未登录,请重新登录",
    404: "资源不存在",
    500: "服务器内部错误",
};

function getErrMsg(code: number): string {
    return errorMessage[code] ?? "未知错误";
}
代码作用
Record<number, string>键必须是 number,值必须是 string
errorMessage[code]按状态码取提示
?? "未知错误"取不到就返回兜底文案(空值合并运算符)

有了 Record<number, string> 约束,误写 errorMessage: { "bad": "出错" } 会立刻类型报错——键的类型不对。

补充:ReturnType 和 Exclude

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

type All = "id" | "name" | "age" | "email";
type AfterExclude = Exclude<All, "email">;  // "id" | "name" | "age"

ReturnType 自动提取函数返回的类型,不用手写;Exclude 是对联合类型做剔除——注意它只处理联合类型,而 Omit 处理的是对象类型,这也是 1.ts 注释里那句"Exclude 处理联合类型,Omit 处理对象接口"的含义。

总结

回头看这道拼多多面试题,其实考了 4 层能力:

  1. 背 API:知道 Pick 挑字段、Omit 删字段
  2. 懂组合Omit = Pick + Exclude + keyof,能手写出来
  3. 明原理keyof 拿联合类型、Exclude 操作联合类型、Pick 操作对象类型
  4. 会应用:知道列表用 Pick、PATCH 用 Partial、字典表用 Record

记忆口诀:keyof 数钥匙 🔑 → Exclude 丢钥匙 → Pick 捡钥匙。下次面试官再让你手写,你就按这个思路拆。

进一步可以试试手写 PartialReadonlyRequired(都用 in 操作符遍历键),理解了"积木组合",这些都能自己拼出来。你学 TS 工具类型时有没有遇到过什么记不住的场景?欢迎评论区聊聊 👏

完整项目代码

项目已托管在 Gitee:

🔗 gitee.com/dcx2758/ai_…

文件树:

backend/interview/ts/pick_partial/
├── readme.md      # 面试题笔记 + 知识点
├── 1.ts           # Pick/Omit/Partial/Record/ReturnType 应用场景
└── 2.ts           # 手写 Omit 实现(面试答案)

快速验证(2 步):

# 1. 克隆仓库并进入目录
git clone git@gitee.com:dcx2758/ai_doubao_dcx.git
cd ai_doubao_dcx/backend/interview/ts/pick_partial

# 2. 用 tsc 做类型检查(零报错即通过)
npx tsc --noEmit --strict 1.ts 2.ts

依赖清单:

依赖版本要求说明
TypeScript≥ 4.1内置工具类型 Omit 自 3.5 引入,4.1 后完整支持
Node.js≥ 16运行 npx 即可,无需装其他依赖

2.ts 里的 MyOmitUser 换成任意接口试手,就能验证你对工具类型组合的理解了 🎯