在前面的章节中,我们深入探讨了泛型的各种硬核语法。但当你真正步入大厂,翻开 Vue、React 或者 NestJS 的源码时,会发现架构师们很少会从零开始手写复杂的泛型推导。
为什么呢?这是因为针对日常开发中最常见的类型转换需求,TypeScript 官方已经为我们提前封装好了一套开箱即用的 “内置泛型工具类型”。
在这一节中,我们将重点攻克日常出场率高达 99% 的五大金刚:Partial、Readonly、Pick、Omit、Record。
TypeScript Partial<T> 类型
在 TypeScript 中,Partial 类型的作用是:接收一个接口 “T”,并将这个接口中的所有属性全部变成 “可选属性”(即在属性名后面自动加上 “?”)。
提示: Partial 的字面意思是 “部分的、局部的”。
假设我们有一个非常庞大的 User 接口,包含了用户的各种信息。现在我们要写一个 updateUser()(更新用户)的函数。在实际业务中,用户可能只修改了 age,也可能只修改了 email。这就意味着我们传入的更新包 updateInfo 里,字段是不完整的。
如果不使用工具类型,我们可能会被迫去新建一个 UserUpdateDto 接口,把所有字段手动加上 “?”。
示例 1:使用 Partial 优雅实现局部更新
// 定义 User 接口(所有字段必填)
interface User {
id: number;
name: string;
age: number;
email: string;
}
// 使用 Partial<User> 将所有字段变成 “可选”
function updateUser(id: number, updateInfo: Partial<User>): void {
console.log("正在更新的用户 ID:", id);
console.log("更新的数据:", updateInfo);
}
// 只传 age
updateUser(1001, { age: 18 });
// 只传 name 和 email
updateUser(1002, { name: "Jack", email: "jack@lvyenet.com" });运行结果如下。
正在更新的用户 ID:1001
更新的数据:{ age: 18 }
正在更新的用户 ID:1002
更新的数据:{ name: 'Jack', email: 'jack@lvyenet.com' }分析:
在 TypeScript 的底层类型演算中,Partial<User> 等价于自动生成了下面这样一个结构:
interface PartialUser {
id?: number;
name?: string;
age?: number;
email?: string;
}也就是说,我们不需要多写一行废话,就完美实现了非常安全的数据更新模型.
TypeScript Readonly<T> 类型
在 TypeScript 中,Readonly<T> 的作用是:接收一个接口 “T”,并将这个接口中的所有属性全部变成 “只读属性”,也就是在属性名前面自动加上 readonly 修饰符。
提示: Readonly 的字面意思是 “只读的”。
在使用 Vue 的 Pinia 或 React 的 Redux,全局状态或核心配置往往都是不能随便轻易就被改动的。我们希望把对象传给业务组件时,组件只能看,但绝对不能改。此时,我们就可以使用 Readonly<T> 来从类型层面彻底封死意外篡改数据的可能。
示例 2:使用 Readonly 锁定配置对象
// 定义核心配置接口
interface AppConfig {
theme: string;
apiUrl: string;
}
// 使用 Readonly 将所有字段锁定为 “只读”
type LockedConfig = Readonly<AppConfig>;
const config: LockedConfig = {
theme: "dark",
apiUrl: "https://api.lvyenet.com"
};
// 读取属性,完全没问题
console.log("当前主题:", config.theme);
// 企图修改属性,TypeScript 瞬间拦截并无情报错
// config.theme = "light";
// 报错:无法为“theme”赋值,因为它是只读属性。分析:
Readonly<AppConfig> 相当于为所有属性加上了锁,它其实等价于:
interface ReadonlyAppConfig {
readonly theme: string;
readonly apiUrl: string;
}通过这种方式,我们十分优雅地保证了数据的单向数据流和绝对安全。
TypeScript Pick<T, K> 类型
在 TypeScript 中,Pick<T, K> 类型的作用是:从一个已有的接口 “T” 中,精准地挑出几个想要的属性 “K”,然后重新组合成一个新的类型。
提示: Pick 的字面意思是 “挑选”。
假设在系统的首页,我们需要展示一个 “精简版” 的用户卡片,只需要用到用户的 name 和 email。如果直接把完整的 User 接口赋给卡片组件,就违反了 “接口隔离原则”(暴露了不需要的多余数据)。
示例 3:使用 Pick 提取局部视图
interface User {
id: number;
name: string;
age: number;
email: string;
address: string;
}
// 挑选 "name" 和 "email" 组成新类型
type UserCardInfo = Pick<User, "name" | "email">;
const card: UserCardInfo = {
name: "Jack",
email: "jack@lvyenet.com"
// 如果企图多传属性(如 age)或者少传,TypeScript 会立刻报错拦截
};
console.log("用户卡片渲染:", card.name);运行结果如下。
用户卡片渲染:Jack分析:
在这个例子中,我们使用 Pick 类型从 User 接口中,精准挑选 "name" 和 "email" 组成新类型。此时 UserCardInfo 完美等价于
{ name: string; email: string; }如果想要挑选多个,我们只需要使用联合类型(即使用 "|")分隔即可。
TypeScript Omit<T, K> 类型
在 TypeScript 中,Omit<T, K> 和 Pick<T, K> 的功能刚好相反,它的作用是:从一个已有的接口 “T” 中,删掉指定的属性 K,并保留剩下的所有属性组成新的类型。
提示: Omit 的字面意思是 “省略、剔除”。
假设我们要写一个 “用户注册” 的表单接口。用户在前端注册时,需要填入姓名、年龄、邮箱等信息。但是!数据库的 id 是后端自动生成的,前端注册时绝对不可能、也不允许提交 id 这个字段。
如果一个接口有 50 个字段,除了 id 之外其他 49 个字段前端都要传。如果我们使用 Pick 去挑选,要写 49 个字段名,也不现实。这时候,使用反向剔除的 Omit 就变得非常简单了。
示例 4:使用 Omit 剔除敏感字段
interface User {
id: number;
name: string;
age: number;
email: string;
address: string;
}
// 从 User 接口中,把 "id" 字段砍掉
type UserRegisterDto = Omit<User, "id">;
const newRegister: UserRegisterDto = {
name: "Lucy",
age: 20,
email: "lucy@leafcoding.com",
address: "广州市天河区"
};
console.log(newRegister.name);运行结果如下。
Lucy分析:
在前后端分离的项目中,Omit 是处理 DTO(数据传输对象)的核心。通过一刀切除 id、createTime、updateTime 等系统字段,我们可以快速基于数据库实体(Entity)生成前端的创建表单类型。
TypeScript Record<K, T> 类型
在 TypeScript 中,Record<K, T> 的作用非常暴力:它可以快速创建一个对象类型,强制规定这个对象的所有 “键(Key)” 的类型必须是 K,所有 “值(Value)” 的类型必须是 T。
提示: Record 的字面意思是 “记录、映射”。
假设我们在开发一个后台管理系统,需要维护一个 “页面权限映射表”。这个映射表的键是任意的页面路由名称(字符串),值则是表示是否有权限的布尔值。如果使用传统的索引签名语法,写起来不够直观。这时候使用 Record 就能极大提升代码的逼格和可读性。
示例 5:使用 Record 定义配置映射表
// 快速定义一个对象:键是 string、值是 boolean
type PermissionMap = Record<string, boolean>;
const userPermissions: PermissionMap = {
"home": true,
"dashboard": true,
"admin": false
};
console.log("是否有进入后台的权限:", userPermissions["admin"]);运行结果如下。
是否有进入后台的权限:false分析:
Record<string, boolean> 等价于定义了下面这样一个传统的接口:
interface PermissionMap {
[key: string]: boolean;
}当我们在写 Vue 或 React 组件时,遇到需要定义复杂字典或者映射表的场景(例如状态码对应错误提示中文的字典),使用 Record 绝对是能够极大提升开发效率的不二法门。
TypeScript 五大内置泛型类型总结
为了方便小伙伴们记忆和在实际项目中快速翻阅,我们把出场率高达 99% 的 “五大金刚” 总结成了下面这张表格。
| 类型 | 作用 | 场景 |
|---|---|---|
| Partial<T> | 将对象的所有属性变为 “可选” | 发起局部更新请求(如只修改密码)、保存表单草稿 |
| Readonly<T> | 将这个接口中的所有属性全部变成“只读属性” | 锁定全局状态(如 Pinia 或 Redux 数据)、保护核心配置对象防止意外篡改 |
| Pick<T, K> | 精准 “挑选” 原对象的部分属性 | 列表页精简卡片展示、提取局部视图所需数据 |
| Omit<T, K> | 无情 “剔除” 原对象的部分属性 | 提交注册表单前剔除系统自增 id、过滤返回数据中的密码字段 |
| Record<K, T> | 批量生成 “键值对” 类型一致的对象 | 定义状态码映射字典、权限配置表、本地缓存结构 |
