← 返回AI变现
🌐 其他

TypeScript 工具类型一次讲透:Pick、Omit、Partial、Record 逐个击破

来源:掘金 · 发布于 2026-08-19 12:44:01
刚学 TypeScript 那会儿,我有个 User 接口到处用:列表页要展示 id + name,编辑页要提交 id + name + age,而登录后返回的接口数据又不能把 email 暴露

TypeScript 工具类型一次讲透:Pick、Omit、Partial、Record 逐个击破

黄敬峰 2026-08-19 10 阅读12分钟

"接口字段要挑几个、删几个、全部改可选……我一开始全是手写新 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 接口展开,先记住这个"主角",后面每个类型都是对它的改造。

目录

一、工具类型到底解决什么问题?

先用大白话讲清楚痛点,再讲它是什么。

假设我们有一个用户接口:

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

现在有个需求:列表页只需要 id 和 name,其他字段都不想要。

"笨办法"是重新写一个 interface:

// ❌ 笨办法:手写一个精简版,字段和 User 是复制粘贴的关系
interface UserPreview {
  id: number;
  name: string;
}

字段一多、接口一变,你就得人肉同步好几份,改漏一个就出 bug。

工具类型要解决的,就是这个"从已有类型派生新类型"的活儿:

// ✅ 一句话:从 User 里挑出 id 和 name,生成新类型
type UserPreview = Pick<User, 'id' | 'name'>;

大白话解释

概念一句人话类比
工具类型拿来一个旧类型,加工出一个新类型一个"类型加工厂"
`Pick<User, 'id''name'>`从 User 里挑出 id、name 两个字段菜单点菜:勾选几个菜

💡 核心思想:工具类型不会凭空造类型,它一定是基于一个已有类型做变换。这个"已有类型"就是下文反复出现的 User。

下面按"地基 → 正向挑 → 反向删 → 原理 → 其他几个"的顺序,逐个击破。

二、keyof:拿到所有键的联合类型(地基)

学其他工具类型之前,必须先搞懂 keyof——它是后面所有人的地基。

大白话

keyof T 会把类型 T 的所有键名抽出来,拼成一个联合类型(| 连接的字符串)。

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' | 'age' | 'email'花名册上的名字清单

💡 一句话记住:keyof T = 拿到 T 的所有键,拼成一个 | 联合类型。后面 Pick、Omit、Exclude 全靠它。

三、Pick:从类型里挑字段

Pick<T, K> 从类型 T 里挑出 K 指定的那些字段,组成新类型。K 就是要挑的键的联合字符串。

大白话

Pick = 点菜。菜单就是 User,你勾选哪几个字段,最后端上桌的就只有那几样。

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'>从 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",剩下的全给你。

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 拿到所有键的联合类型

type UserKeys = keyof User;
// 'id' | 'name' | 'age' | 'email'

第二步:Exclude 把要剔除的 K 键删掉,剩下需要保留的键

type KeepKeys = Exclude<UserKeys, 'email'>;
// 'id' | 'name' | 'age'   —— email 被从联合类型里删掉了

第三步:再用 Pick 把剩下的键从类型 T 里挑出来,就实现了 Omit 的效果

type MyOmitUser = Pick<User, KeepKeys>;
// 等价于 Omit<User, 'email'>

一张图看懂数据流

   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 里能找到):

// 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 = 把必答的填空题,全部改成选做题——你可以一个都不填。

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> 当参数类型,就允许只传部分字段,不用每改一个字段就重新写一个接口。

// 只改邮箱,其他字段都不用传
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 是某类型"的一张表。

// 一个字符串到数字的字典: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 最经典的用法,把状态码映射成一句人话:

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 = 让函数"自己交代"它返回了什么,不用你人肉去看。

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 直接报错:

// ❌ 错误:ReturnType<fn> —— fn 是值不是类型,报错
// type Wrong = ReturnType<fn>;

// ✅ 正确:用 typeof 拿到函数的类型
type Right = ReturnType<typeof fn>;

💡 一句话记住:ReturnType<typeof fn>,typeof 千万别漏。

九、Exclude:联合类型的差集

Exclude<T, U> 从联合类型 T 里剔除掉 U 里出现的成员。

大白话

Exclude = 从名单里划掉几个人。注意它处理的是联合类型,不是对象。

type All = 'id' | 'name' | 'age' | 'email';
type After = Exclude<All, 'email'>;
// After = 'id' | 'name' | 'age'  —— email 被划掉了

Exclude 和 Omit 别搞混(高频考点)

⚠️ 面试考点:Exclude 和 Omit 都是"剔除",但作用对象完全不同。

维度ExcludeOmit
作用对象联合类型('a' | 'b' | 'c')对象接口(interface)
剔除什么联合类型里的某个成员对象里的某个字段
例子Exclude<'a'|'b'|'c', 'b'>Omit<User, 'email'>
类比名单里划掉名字删掉对象的一个属性

💡 一句话记住:Exclude 处理联合类型,Omit 处理对象接口。二者一个删"成员",一个删"字段"。

十、终极速查表 + 面试高频 N 问

工具类型速查表

工具类型语法作用一句话
keyofkeyof T拿所有键的联合类型花名册
PickPick<T, K>挑出 K 指定的字段点菜
OmitOmit<T, K>删掉 K 指定的字段拉黑
PartialPartial<T>所有字段变可选选做题
RecordRecord<K, V>键值对字典电话簿
ReturnTypeReturnType<typeof fn>拿函数返回类型让它自己交代
ExcludeExclude<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 问

1. 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 行记住全部):

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 面试备战