内置工具类型
前面几章我们学会了写类型:接口、联合、泛型、映射类型、条件类型。但真实项目里,很多类型不是「从零写出来」的,而是从已有类型变换出来的:
- 表单提交时要求所有字段齐全,草稿保存时允许只填一部分;
- 数据库返回完整的
User,但接口只想暴露其中三个字段; - 一个函数已经写好了,我想拿到它的返回值类型,而不是手抄一遍。
TypeScript 在 lib.es5.d.ts 里内置了一批工具类型(Utility Types),专门做这些变换。它们不需要 import,全局可用。
本章的每个工具类型,你都能用前面学过的映射类型 + 条件类型 + infer 亲手实现。会用是及格,能手写实现才算真正理解。
1. 属性修饰类:Partial / Required / Readonly
这三个只改修饰符,不改属性集合。
type Partial<T> = { [K in keyof T]?: T[K] };
type Required<T> = { [K in keyof T]-?: T[K] };
type Readonly<T> = { readonly [K in keyof T]: T[K] };关键在映射类型的修饰符语法:? 表示加上可选,-? 表示去掉可选,同理还有 readonly 与 -readonly。
典型用途是「更新补丁」:更新用户时,客户端只传改动的字段。
type User = { id: number; name: string; email?: string };
// 1) Partial: 全部变可选,适合「补丁对象」
const patch: Partial<User> = { name: "Ada" };
console.log("Partial:", JSON.stringify(patch));
// 2) Required: 全部变必填,email 也不能少了
const full: Required<User> = { id: 1, name: "Ada", email: "ada@example.com" };
console.log("Required:", JSON.stringify(full));
// 3) Readonly: 全部只读
const frozen: Readonly<User> = full;
// frozen.id = 2; // 若取消注释会报错:无法为只读属性赋值
console.log("Readonly id:", frozen.id);
// 4) 手写实现
type MyPartial<T> = { [K in keyof T]?: T[K] };
type MyRequired<T> = { [K in keyof T]-?: T[K] };
type MyReadonly<T> = { readonly [K in keyof T]: T[K] };
const mine: MyPartial<User> = { email: "x@y.z" };
console.log("MyPartial:", JSON.stringify(mine));
function applyPatch(base: User, p: Partial<User>): User {
return Object.assign({}, base, p);
}
console.log("merged:", JSON.stringify(applyPatch(full, { name: "Grace" })));Partial 只处理第一层属性。嵌套对象里的字段仍然是必填的。需要递归时要自己写 DeepPartial,我们在下一章会实现它。
2. 属性筛选类:Pick / Omit / Record
2.1 Pick<T, K> 挑出若干键
type Pick<T, K extends keyof T> = { [P in K]: T[P] };K extends keyof T 这个约束很重要:它保证你只能挑存在的键,写错字段名会立即报错。
2.2 Omit<T, K> 排除若干键
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;注意 Omit 的第二个参数约束是 keyof any(即 string | number | symbol)而不是 keyof T。这是官方有意为之的宽松设计:排除一个不存在的键不算错误。代价是拼错字段名不会被发现。
2.3 Record<K, V> 造一张表
type Record<K extends keyof any, V> = { [P in K]: V };Record 是构造「字典」最常用的方式。当 K 是字面量联合时,它还能强制你穷尽所有 key——少写一个就编译不过,这在做多语言文案、状态机配置表时非常有用。
type Todo = { id: number; title: string; done: boolean; owner: string };
type TodoPreview = Pick<Todo, "id" | "title">;
const preview: TodoPreview = { id: 1, title: "写 TS 教程" };
console.log("Pick:", JSON.stringify(preview));
type PublicTodo = Omit<Todo, "owner">;
const pub: PublicTodo = { id: 2, title: "复习类型", done: false };
console.log("Omit:", JSON.stringify(pub));
type Page = "home" | "about" | "contact";
// 少写一个 key 就会编译报错,天然的穷尽性检查
const titles: Record<Page, string> = {
home: "首页",
about: "关于我们",
contact: "联系我们",
};
console.log("Record:", JSON.stringify(titles));
// 手写实现
type MyPick<T, K extends keyof T> = { [P in K]: T[P] };
type MyRecord<K extends keyof any, V> = { [P in K]: V };
type MyOmit<T, K extends keyof any> = MyPick<T, Exclude<keyof T, K>>;
const slim: MyOmit<Todo, "owner" | "done"> = { id: 3, title: "手写 Omit" };
console.log("MyOmit:", JSON.stringify(slim));
function pick<T extends object, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> {
const out = {} as Pick<T, K>;
for (const k of keys) out[k] = obj[k];
return out;
}
const t: Todo = { id: 4, title: "运行一下", done: true, owner: "me" };
console.log("运行时 pick:", JSON.stringify(pick(t, ["id", "done"])));如果你希望排除不存在的键时报错,可以自己写一个更严格的版本:type StrictOmit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>。在自己的项目里用它替代 Omit,能少踩很多重构时的坑。
3. 联合筛选类:Exclude / Extract / NonNullable
这三个作用在联合类型上,靠的是条件类型的**分发(distributive)**特性:当条件类型作用于裸类型参数且实参是联合时,会逐个成员分别求值再合并。
type Exclude<T, U> = T extends U ? never : T;
type Extract<T, U> = T extends U ? T : never;
type NonNullable<T> = T & {}; // TS 4.9 起改成了交叉写法以 Exclude<"a" | "b", "a"> 为例,它会展开成 ("a" extends "a" ? never : "a") | ("b" extends "a" ? never : "b"),也就是 never | "b",最终得到 "b"。
| 工具类型 | 含义 | 例子 |
|---|---|---|
Exclude<T, U> | 从联合 T 中去掉能赋给 U 的成员 | Exclude<"a" | "b", "a"> 得 "b" |
Extract<T, U> | 只保留能赋给 U 的成员 | Extract<string | number, string> 得 string |
NonNullable<T> | 去掉 null 与 undefined | NonNullable<string | null> 得 string |
4. 函数相关类:ReturnType / Parameters / Awaited
这一组的核心是 infer:在条件类型里「挖一个洞」,让编译器把匹配到的类型填进来。
type ReturnType<T extends (...a: any) => any> = T extends (...a: any) => infer R ? R : any;
type Parameters<T extends (...a: any) => any> = T extends (...a: infer P) => any ? P : never;Parameters 返回的是一个元组类型,可以配合展开运算符调用函数,这是写包装器(缓存、重试、日志)的基础。
Awaited<T> 则会递归地剥掉 Promise:Awaited<Promise<Promise<number>>> 就是 number。它取代了早年大家手写的 UnwrapPromise。
function makeUser(id: number, name: string) {
return { id, name, createdAt: "2026-01-01" };
}
type UserShape = ReturnType<typeof makeUser>;
type UserArgs = Parameters<typeof makeUser>;
const args: UserArgs = [7, "Grace"];
const u: UserShape = makeUser(...args);
console.log("ReturnType:", JSON.stringify(u));
async function fetchCount(): Promise<number> {
return 42;
}
type Count = Awaited<ReturnType<typeof fetchCount>>;
const c: Count = 42;
console.log("Awaited:", c);
// 手写 ReturnType
type MyReturnType<T extends (...a: never[]) => unknown> =
T extends (...a: never[]) => infer R ? R : never;
const same: MyReturnType<typeof makeUser> = u;
console.log("MyReturnType 通过:", same.name);
// 用「参数元组 + 返回值」两个类型参数写一个通用日志包装器
function withLog<A extends unknown[], R>(name: string, fn: (...a: A) => R) {
return (...args: A): R => {
console.log("调用", name, "参数:", JSON.stringify(args));
return fn(...args);
};
}
const logged = withLog("makeUser", makeUser);
type LoggedArgs = Parameters<typeof logged>;
const nextArgs: LoggedArgs = [9, "Linus"];
console.log("结果:", JSON.stringify(logged(...nextArgs)));
type Kind = "a" | "b" | 1 | 2;
const onlyStr: Extract<Kind, string> = "a";
const onlyNum: Exclude<Kind, string> = 2;
const notNull: NonNullable<string | null | undefined> = "safe";
console.log(onlyStr, onlyNum, notNull);ReturnType<makeUser> 是错的,必须写成 ReturnType<typeof makeUser>。类型位置上的 typeof 是类型查询,和 JS 运行时的 typeof 运算符是两回事,只是复用了关键字。
5. 完整速查表
| 工具类型 | 一句话 |
|---|---|
Partial<T> | 全部属性变可选 |
Required<T> | 全部属性变必填 |
Readonly<T> | 全部属性变只读 |
Pick<T, K> | 只保留 K 这些键 |
Omit<T, K> | 去掉 K 这些键 |
Record<K, V> | 键为 K、值为 V 的对象 |
Exclude<T, U> | 联合中去掉 U |
Extract<T, U> | 联合中只留 U |
NonNullable<T> | 去掉 null 与 undefined |
ReturnType<F> | 函数返回值类型 |
Parameters<F> | 函数参数元组 |
ConstructorParameters<C> | 构造函数参数元组 |
InstanceType<C> | 构造函数的实例类型 |
Awaited<T> | 递归解开 Promise |
- 手写
MyExtract<T, U>与MyNonNullable<T>,并用const变量验证它们和内置版本行为一致。 - 定义
type Article = { id: number; title: string; body: string; authorId: number },用工具类型组合出「创建文章的入参类型」:不含id,且body可选。提示:Omit之后再和Partial<Pick<...>>做交叉。 - 写一个
type DeepPartial<T>,让嵌套对象的每一层属性都变成可选。
小结
- 工具类型 = 映射类型 + 条件类型 +
infer的固定组合,全局可用无需 import Partial/Required/Readonly改修饰符;Pick/Omit/Record改属性集合Exclude/Extract/NonNullable依赖条件类型的分发特性,作用在联合上ReturnType/Parameters/Awaited靠infer从已有函数或 Promise 里「反查」类型- 下一章进入模板字面量类型与类型体操 →