TypeScript 工具类型一篇讲透:Pick、Omit、Partial、Exclude、Record、ReturnType、keyof

TypeScript 工具类型一篇讲透:Pick、Omit、Partial、Exclude、Record、ReturnType、keyof

本文是一篇 TypeScript 学习日志。高级类型(工具类型)是 TS 进阶的必经之路,也是面试高频考点。本文用一个 User 类型贯穿始终,把最常用的 7 个工具类型 ------keyofPickOmitPartialExcludeRecordReturnType 逐个讲清,并手写它们的源码实现,让你彻底搞懂 Omit 为什么等价于 Pick<T, Exclude<keyof T, K>>

目录


一、为什么要学工具类型

先看一个最常出现的需求:「一个类型里,挑选一些需要的字段,形成一个新的类型」

假设我们定义了一个完整的 User 类型:

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

在实际项目里,我们几乎不会一直用「全量」的 User,而是会遇到各种「变形」需求:

  • 展示用户列表,只要 idname(不要 email 这种敏感字段);
  • 返回给第三方,排除 email
  • 修改用户信息,字段全部可选(PATCH 部分更新);
  • 定义 HTTP 状态码 → 错误文案的映射表

这些需求如果每个都手动重新定义类型,字段一多就会大量重复、难以维护。工具类型 就是 TypeScript 官方和内置的「类型变形函数」,帮你从已有类型派生出新类型。

一句话理解:工具类型 = 操作类型的函数,输入一个类型,输出一个新的类型。

下面用同一个 User 类型,把最常用的几个工具类型逐个吃透。


二、keyof:获取所有属性名

keyof T 的作用:从一个类型 T 中,取出所有的属性名,组成一个联合类型

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

type UserKeys = keyof User;
// type UserKeys = 'id' | 'name' | 'age' | 'sex' | 'email'
  • keyof User 得到的是 'id' | 'name' | 'age' | 'sex' | 'email'
  • 注意:这是字符串字面量类型的联合 ,不是普通的 string

keyof 是所有「对象类型工具」的地基,后面的 PickOmit 都建立在它之上。


三、Pick:从类型里挑字段

3.1 用法

Pick<T, K> 的作用:从类型 T 中,选取 K 指定的属性,形成一个新的类型

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

const u: UserPrivate = {
  id: 1,
  name: 'zj',
};
// ✅ 只允许 id 和 name,多写或少写都会报错

UserPrivate 等价于手写:

typescript 复制代码
type UserPrivate = {
  id: number;
  name: string;
};

3.2 源码实现

Pick 官方源码只有一行:

typescript 复制代码
type Pick<T, K extends keyof T> = {
  [P in K]: T[P];
};

拆解:

  • K extends keyof T约束 K 必须是 T 的某个属性名(否则编译报错);
  • [P in K]映射类型,遍历联合类型 K 里的每个属性名 P;
  • T[P]索引访问类型,取出 T 里 P 对应的值类型。

典型场景 :对外接口只暴露部分字段、做权限控制(UserPrivate 不带 email 敏感信息)。


四、Omit:从类型里删字段

4.1 用法

Omit<T, K> 的作用:从类型 T 中,排除 K 指定的属性,形成一个新的类型 (和 Pick 正好相反)。

typescript 复制代码
type UserSafe = Omit<User, 'email'>;

const uSafe: UserSafe = {
  id: 2,
  name: 'zj',
  age: 18,
  sex: '男',
};
// ✅ 除 email 外都要有

UserSafe 等价于:

typescript 复制代码
type UserSafe = {
  id: number;
  name: string;
  age: number;
  sex: string;
};

4.2 源码实现:Omit 为什么 = Pick + Exclude + keyof

这是本文最核心的一个知识点。Omit 官方源码:

typescript 复制代码
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

你可能会在面试里被问到:Omit<T, K> 等价于 Pick<T, Exclude<keyof T, K>>,怎么理解?」

我们一步步拆:

第一步 keyof T:取出 T 的所有属性名。

typescript 复制代码
keyof User
// → 'id' | 'name' | 'age' | 'sex' | 'email'

第二步 Exclude<keyof T, K>:从「所有属性名」里排除掉 K。

typescript 复制代码
Exclude<'id' | 'name' | 'age' | 'sex' | 'email', 'email'>
// → 'id' | 'name' | 'age' | 'sex'   (去掉了 email)

第三步 Pick<T, ...>:用剩下的属性名,从 T 里挑出对应的字段。

typescript 复制代码
Pick<User, 'id' | 'name' | 'age' | 'sex'>
// → { id: number; name: string; age: number; sex: string }

所以 Omit 的完整逻辑就是:「先算要保留哪些字段(keyof 全量 - 排除项),再用 Pick 挑出来」

💡 一句话记牢:Omit = 先 Exclude 求出要保留的 key,再 Pick 出来


五、Partial:所有字段变可选

5.1 用法

Partial<T> 的作用:把类型 T 的所有字段都变成可选(?

typescript 复制代码
type PartialUser = Partial<User>;

const uPartial: PartialUser = {
  id: 3,
  name: 'zj',
  sex: '男',
};
// ✅ 字段可写可不写,写几个都行

const emptyObj: PartialUser = {};   // ✅ 甚至可以为空对象

5.2 源码实现

typescript 复制代码
type Partial<T> = {
  [P in keyof T]?: T[P];
};

拆解:

  • [P in keyof T]:遍历 T 的所有属性名;
  • ?:给每个属性加「可选」修饰符;
  • T[P]:保留原来的值类型。

典型场景PATCH 接口的部分更新。比如 NestJS 里的 update(id, patch: Partial<Todo>),前端只传要改的字段即可,不用传全量。


六、Exclude:从联合类型里排除

6.1 用法

Exclude<T, U> 的作用:从联合类型 T 中,排除掉 U 里出现的类型

typescript 复制代码
type All = 'id' | 'name' | 'age' | 'sex' | 'email';

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

6.2 源码实现

typescript 复制代码
type Exclude<T, U> = T extends U ? never : T;

这里用到了**「分布式条件类型」**:

  • 当 T 是联合类型时,T extends U ? never : T把联合类型的每个成员单独判断
  • 如果某个成员属于 U,就变成 never(从结果里去掉);
  • 否则保留该成员。

💡 关键区分:Exclude 处理的是「联合类型」 (比如 keyof User 的结果),Omit 处理的是「对象类型」 (比如 User)。别搞混。


七、Record:构造 key-value 映射

7.1 用法

Record<K, V> 的作用:构造一个对象类型,key 的类型是 K,value 的类型是 V

typescript 复制代码
type Dict = Record<string, number>;

const obj: Dict = {
  a: 1,
  b: 2,
};
// ✅ 键是 string,值是 number

最经典的实战场景:HTTP 状态码 → 错误文案的映射表

typescript 复制代码
// key 是 number(状态码),value 是 string(错误文案)
type ErrorMsgMap = Record<number, string>;

const errorMsg: ErrorMsgMap = {
  400: '请求参数错误',
  401: '未登录',
  402: '未支付',
  403: '权限不足',
  404: '资源不存在',
  500: '服务器错误',
};

function getErrorMsg(code: number) {
  return errorMsg[code] ?? '未知错误';
}

getErrorMsg(404); // '资源不存在'
getErrorMsg(999); // '未知错误'

7.2 HTTP 状态码科普(顺手记住)

范围 含义
1xx 执行中 / 信息响应
2xx 成功
3xx 跳转 / 重定向
4xx 客户端错误
5xx 服务器错误

7.3 源码实现

typescript 复制代码
type Record<K extends keyof any, T> = {
  [P in K]: T;
};
  • K extends keyof any:约束 K 必须是合法的键类型(string / number / symbol);
  • [P in K]: T:遍历 K 的每个键,值类型统一为 T。

八、ReturnType:获取函数返回值类型

8.1 用法

ReturnType<T> 的作用:获取一个函数类型 T 的返回值类型

typescript 复制代码
function fn() {
  return { x: 1, y: 2 };
}

type FnReturn = ReturnType<typeof fn>;
// type FnReturn = { x: number; y: number }
  • typeof fn:拿到函数 fn 的类型(不是调用它);
  • ReturnType<...>:提取这个函数类型的返回值类型。

8.2 源码实现

typescript 复制代码
type ReturnType<T extends (...args: any) => any> =
  T extends (...args: any) => infer R ? R : any;

这里用到了 infer 推断

  • infer R:让 TS 自动推断返回值类型,并用 R 占位;
  • 如果 T 是函数类型,就返回推断出的 R;否则返回 any

典型场景 :复用某个函数的返回值类型,避免手写重复;或封装 Redux 里的 RootState = ReturnType<typeof store.getState>


九、综合实战

把上面的工具类型串起来,看一个贴近真实项目的例子:

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

// 1. 列表展示:只要 id 和 name
type UserListItem = Pick<User, 'id' | 'name'>;

// 2. 对外安全返回:排除 email
type PublicUser = Omit<User, 'email'>;

// 3. 部分更新:所有字段可选
type UpdateUser = Partial<User>;

// 4. 手写 MyOmit(理解 Omit 的底层)
type UserKeys = keyof User;                         // 所有属性名
type KeepKeys = Exclude<UserKeys, 'email'>;         // 排除 email
type MyOmitUser = Pick<User, KeepKeys>;             // 用剩余 key 挑字段
// MyOmitUser 和 Omit<User, 'email'> 完全等价

// 5. 状态码映射
type StatusMap = Record<number, string>;

// 6. 提取函数返回值
function createUser() {
  return { id: 1, name: 'zj', email: 'z@j.com' };
}
type CreatedUser = ReturnType<typeof createUser>;

十、总结

这篇文章用同一个 User 类型,把最常用的工具类型串了一遍:

工具类型 作用 一句话
keyof T 取所有属性名 得到 `'a'
Pick<T, K> 挑字段 从 T 选 K 属性
Omit<T, K> 删字段 从 T 删 K 属性
Partial<T> 全部可选 字段都加 ?
Exclude<T, U> 排除联合成员 从联合类型里去掉 U
Record<K, V> 造映射 key 是 K,value 是 V
ReturnType<T> 取函数返回类型 infer 推断

三个必背的面试点

  1. Omit<T, K> = Pick<T, Exclude<keyof T, K>>------先排除求出保留 key,再 Pick;
  2. Exclude 处理联合类型,Omit 处理对象类型
  3. Partial 的核心用途是 PATCH 部分更新ReturnTypeinfer 实现。

一句话总结 :工具类型就是「操作类型的函数」,掌握了 keyof(取键)、映射类型 [P in K](遍历)、条件类型 + infer(推断)这三个地基,Pick/Omit/Partial/Record/ReturnType 全都信手拈来。


相关推荐
用户2181697049301 小时前
Flutter (十九) Tabbar
前端
默_笙1 小时前
🍳 受控组件和非受控组件,我纠结了一整天,最后用"房东和租客"讲明白了
前端·javascript
烬羽1 小时前
CSS 三栏布局:`margin-left: -100%` 凭什么把侧栏「拽」回上一行?彻底搞懂负 margin
css·面试·前端框架
缓冲中请稍后1 小时前
前端HTTP请求完全指南:从基础到LLM接口调用实战
前端·面试
半个落月1 小时前
React 受控组件与非受控组件详解:从输入框到表单校验
前端·react.js
BreezeJiang1 小时前
写了 display:flex,为什么三栏布局还没完成?
前端·css
小林ixn1 小时前
TS 工具类型实战:Pick/Omit/Partial/Record 一次讲透,别再 Omit 反了
typescript·代码规范
Hilaku1 小时前
高级前端如何优雅地拒绝不合理的产品需求?
前端·javascript·程序员
独立开阀者_FwtCoder1 小时前
这次更新:换新主题,同时把好计划分享出去
前端·javascript·vue.js