内置工具类型(上)
本教程共 80 篇 · 第 41 篇 · 更新于 2026-08-10 · 约 10 分钟阅读
本节目标:掌握五个高频工具类型——
Partial、Required、Readonly、Pick、Omit——不仅会用,还能自己”手写”出来,理解它们不是魔法。
TypeScript 内置了一批工具类型(Utility Types),全部定义在 lib.es5.d.ts 里,不需要额外 import 就能用。它们本质上都是泛型 + 映射类型的组合。看懂实现原理,你就真正理解了 TypeScript 类型系统的表达能力。
这一章先讲最常用的五个。
Partial<Type>:全变可选
它能干什么
Partial<Type> 接收一个对象类型,返回一个所有属性都变成可选的新类型:
interface User {
id: number;
name: string;
email: string;
}
// 全部属性变成了可选的
type PartialUser = Partial<User>;
// 等价于:
// {
// id?: number;
// name?: string;
// email?: string;
// }
典型使用场景:更新操作。你只想改某几个字段,其他字段不传:
function updateUser(id: number, fields: Partial<User>): void {
// fields 里每个属性都是可选的,只传要改的就行
}
updateUser(1, { name: "李四" }); // ✅ 只传 name
updateUser(1, { name: "李四", email: "" }); // ✅ 传一部分
手写实现原理
type MyPartial<T> = {
[K in keyof T]?: T[K];
};
逐行解释:
keyof T:拿到T的所有键名组成的联合类型。[K in keyof T]:遍历每个键K。?: T[K]:把每个属性变成可选(?),值类型保持不变(T[K])。
就这一行代码——这就是 Partial 的全部魔法。它本质是一个映射类型,遍历源类型的所有键,给每个字段加了一个问号。
Required<Type>:全变必选
它能干什么
Required<Type> 是 Partial 的反向操作——把所有可选属性变成必选:
interface Config {
host?: string;
port?: number;
debug?: boolean;
}
// 全部属性变成必选
type StrictConfig = Required<Config>;
// 等价于:
// {
// host: string;
// port: number;
// debug: boolean;
// }
// ❌ 必须提供所有属性
const config: StrictConfig = {
host: "localhost",
port: 3000,
debug: true,
};
手写实现原理
type MyRequired<T> = {
[K in keyof T]-?: T[K];
};
注意这里的 -?——它是映射类型的修饰符操作。? 表示加可选,-? 表示移除可选。对于本来就必选的属性,-? 没有影响。对于可选的属性,-? 把问号去掉,变成必选。
Readonly<Type>:全变只读
它能干什么
Readonly<Type> 把对象所有属性变成只读——创建后不能修改:
interface Todo {
title: string;
completed: boolean;
}
const todo: Readonly<Todo> = {
title: "写完这篇教程",
completed: false,
};
// todo.title = "改个标题"; // ❌ 不能赋值给只读属性
一个很实用的场景:确保配置对象在运行时不被人篡改:
function init(config: Readonly<{ env: string; debug: boolean }>) {
// config.env = "production"; // ❌ 安全——编译期就拦住了
if (config.debug) {
console.log("调试模式开启");
}
}
手写实现原理
type MyReadonly<T> = {
readonly [K in keyof T]: T[K];
};
跟 Partial 几乎一样,只是 ? 换成了 readonly。readonly 是映射类型的另一个修饰符。
Tip如果想把
readonly去掉(“解锁”一个类型),用-readonly:type MyMutable<T> = { -readonly [K in keyof T]: T[K]; };
Pick<Type, Keys>:选取指定键
它能干什么
Pick<Type, Keys> 从 Type 中挑选 Keys 指定的属性,构造一个新类型:
interface User {
id: number;
name: string;
email: string;
password: string;
}
// 只暴露公开信息,不暴露密码
type PublicUser = Pick<User, "id" | "name" | "email">;
// 等价于:
// {
// id: number;
// name: string;
// email: string;
// }
function getPublicProfile(user: User): PublicUser {
return {
id: user.id,
name: user.name,
email: user.email,
};
}
手写实现原理
type MyPick<T, K extends keyof T> = {
[P in K]: T[P];
};
注意 K extends keyof T——这个约束确保你只能选 T 上存在的键名。如果你传一个不存在的键,TypeScript 会在编译期报错:
// ❌ "avatar" 不是 User 的属性
// type Bad = Pick<User, "avatar">;
跟前面几个工具类型不同,Pick 的映射遍历的不是 keyof T,而是 K——也就是你指定的键名联合。
Omit<Type, Keys>:排除指定键
它能干什么
Omit<Type, Keys> 从 Type 中排除 Keys 指定的属性,剩下的组成新类型。跟 Pick 思路相反:
interface User {
id: number;
name: string;
email: string;
password: string;
}
// 排除敏感字段
type SafeUser = Omit<User, "password">;
// 等价于:
// {
// id: number;
// name: string;
// email: string;
// }
// 也可以排除多个
type UserPreview = Omit<User, "password" | "email">;
// 等价于:{ id: number; name: string; }
手写实现原理
type MyOmit<T, K extends keyof T> = {
[P in Exclude<keyof T, K>]: T[P];
};
Omit 比前面几个稍微”复合”一点——它借用了 Exclude 来过滤键名:
keyof T:T的所有键。Exclude<keyof T, K>:从所有键中排除K。[P in Exclude<keyof T, K>]:只遍历剩下的键。
Exclude 是另一个内置工具类型(下一章讲),它的作用是”从联合类型中排除某些成员”。Omit 本质上就是 Pick 的”取反”——挑出不在 K 中的键。
五个工具类型对比
| 工具类型 | 作用 | 修饰符 |
|---|---|---|
Partial<T> | 全部属性变可选 | +? |
Required<T> | 全部属性变必选 | -? |
Readonly<T> | 全部属性变只读 | +readonly |
Pick<T, K> | 选取指定键 | 无 |
Omit<T, K> | 排除指定键 | 无(结合 Exclude) |
小结
这五个工具类型的核心是映射类型语法 [K in keyof T]: ...。Partial/Required/Readonly 是”遍历所有属性 + 加修饰符”,Pick/Omit 是”挑出或排除部分属性”。理解它们的实现方式之后,你可以自己写专属的工具类型——比如”把一个类型的部分属性变成可选”或者”只保留字符串类型的属性”。
这些不是黑魔法,就是一行或两行泛型映射类型。下一章我们继续看进阶工具类型——它们会用到条件类型,表达能力更上一层楼。