TypeScript 自带一批工具类型,它们都是用前面几篇讲的条件类型和映射类型实现的。这一篇是完整目录加组合示例。
完整目录
1// 内置工具类型完整目录(行为均经实测验证)
2
3interface User {
4 id: number;
5 name: string;
6 email: string;
7 age: number;
8}
9
10// ==================== 对象变换 ====================
11type P = Partial<User>; // 全部可选
12type R = Required<P>; // 全部必需
13type RO = Readonly<User>; // 全部只读
14type Picked = Pick<User, "id" | "name">; // 只保留指定键
15type Omitted = Omit<User, "email">; // 排除指定键
16type Rec = Record<"a" | "b", number>; // { a: number; b: number }
17
18// ==================== 联合变换 ====================
19type U = Exclude<"a" | "b" | "c", "a">; // "b" | "c"
20type I = Extract<"a" | "b" | "c", "a" | "b">; // "a" | "b"
21type NN = NonNullable<string | null | undefined>; // string
22
23// ==================== 函数变换 ====================
24type Fn = (this: { id: number }, a: string, b: number) => boolean;
25
26type Ret = ReturnType<Fn>; // boolean
27type Params = Parameters<Fn>; // [string, number]
28type ThisT = ThisParameterType<Fn>; // { id: number }
29type NoThis = OmitThisParameter<Fn>; // (a: string, b: number) => boolean
30
31// 构造函数相关
32class Point { constructor(public x: number, public y: number) {} }
33type CtorParams = ConstructorParameters<typeof Point>; // [number, number]
34type Inst = InstanceType<typeof Point>; // Point
35
36// ==================== Promise ====================
37type A1 = Awaited<Promise<string>>; // string
38type A2 = Awaited<Promise<Promise<number>>>; // number(递归)
39type A3 = Awaited<string | Promise<number>>; // string | number
40
41// ==================== 字符串 ====================
42type S1 = Uppercase<"a">; // "A"
43type S2 = Lowercase<"A">; // "a"
44type S3 = Capitalize<"ab">; // "Ab"
45type S4 = Uncapitalize<"Ab">; // "ab"
46
47// ==================== 组合出项目专用工具 ====================
48
49// 把某个键变成可选
50type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
51
52// 把某个键变成必需
53type RequiredBy<T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>;
54
55// 只改一个键的类型
56type Override<T, K extends keyof T, V> = Omit<T, K> & { [P in K]: V };
57
58// 深度 Partial
59type DeepPartial<T> = {
60 [K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
61};
62
63// 深度 Readonly
64type DeepReadonly<T> = {
65 readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
66};
67
68// 至少一个键必需(互斥或)
69type AtLeastOne<T, K extends keyof T = keyof T> = Omit<T, K> &
70 { [P in K]: Required<Pick<T, P>> & Partial<Record<Exclude<K, P>, never>> }[K];
71
72// 从联合里挑出函数的返回类型
73type UnionReturn<T> = T extends (...args: never[]) => infer R ? R : never;
74
75// 把联合转成交叉
76type UnionToIntersection<U> =
77 (U extends unknown ? (x: U) => void : never) extends (x: infer I) => void ? I : never;
78
79// 取出值为某类型的键
80type KeysOfType<T, V> = { [K in keyof T]: T[K] extends V ? K : never }[keyof T];
81
82// ==================== 验证 ====================
83
84const partial: P = { id: 1 };
85const required: R = { id: 1, name: "a", email: "b", age: 1 };
86const picked: Picked = { id: 1, name: "a" };
87const omitted: Omitted = { id: 1, name: "a", age: 1 };
88const rec: Rec = { a: 1, b: 2 };
89const excl: U = "b";
90const extr: I = "a";
91const nonNull: NN = "s";
92const ret: Ret = true;
93const params: Params = ["s", 1];
94const thisT: ThisT = { id: 1 };
95const noThis: NoThis = (a: string, b: number) => a.length > b;
96const ctorParams: CtorParams = [1, 2];
97const inst: Inst = new Point(1, 2);
98const a1: A1 = "s";
99const a2: A2 = 1;
100const a3: A3 = "s";
101const s1: S1 = "A";
102const s2: S2 = "a";
103const s3: S3 = "Ab";
104const s4: S4 = "ab";
105
106const optEmail: PartialBy<User, "email"> = { id: 1, name: "a", age: 1 };
107const reqEmail: RequiredBy<P, "email"> = { id: 1, email: "b" };
108const strId: Override<User, "id", string> = { id: "1", name: "a", email: "b", age: 1 };
109const deep: DeepPartial<{ a: { b: { c: number } } }> = { a: { b: {} } };
110const deepRo: DeepReadonly<{ a: { b: number } }> = { a: { b: 1 } };
111const atLeast: AtLeastOne<{ a: number; b: string }> = { a: 1 };
112const unionRet: UnionReturn<() => string> = "s";
113const unionInter: UnionToIntersection<{ a: 1 } | { b: 2 }> = { a: 1, b: 2 };
114const keysOfType: KeysOfType<User, string> = "name"; // "name" | "email"
115
116console.log(partial, required, picked, omitted, rec, excl, extr, nonNull, ret, params);
117console.log(thisT, noThis, ctorParams, inst, a1, a2, a3, s1, s2, s3, s4);
118console.log(optEmail, reqEmail, strId, deep, deepRo, atLeast, unionRet, unionInter, keysOfType);
1{ id: 1 } [Function: noThis] [ 1, 2 ] Point { x: 1, y: 2 } s 1 s A a Ab ab
2{ id: 1, name: 'a', age: 1 } { id: 1, email: 'b' } { id: '1', name: 'a', email: 'b', age: 1 } { a: { b: {} } } { a: { b: 1 } } { a: 1 } s { a: 1, b: 2 } name
对象变换
| 工具类型 | 作用 | 实现 |
|---|---|---|
Partial<T> | 全部可选 | { [K in keyof T]?: T[K] } |
Required<T> | 全部必需 | { [K in keyof T]-?: T[K] } |
Readonly<T> | 全部只读 | { readonly [K in keyof T]: T[K] } |
Pick<T, K> | 保留指定键 | { [P in K]: T[P] } |
Omit<T, K> | 排除指定键 | Pick<T, Exclude<keyof T, K>> |
Record<K, V> | 构造键值对象 | { [P in K]: V } |
联合变换
| 工具类型 | 作用 | 实现 |
|---|---|---|
Exclude<T, U> | 排除可赋给 U 的成员 | T extends U ? never : T |
Extract<T, U> | 只保留可赋给 U 的成员 | T extends U ? T : never |
NonNullable<T> | 排除 null/undefined | T extends null | undefined ? never : T |
这三个是分布式条件类型的直接应用(见 条件类型与 infer)。
函数变换
| 工具类型 | 作用 |
|---|---|
ReturnType<T> | 返回类型 |
Parameters<T> | 参数元组 |
ThisParameterType<T> | this 类型 |
OmitThisParameter<T> | 去掉 this 参数 |
ConstructorParameters<T> | 构造函数参数元组 |
InstanceType<T> | 构造函数实例类型 |
ThisParameterType 和 OmitThisParameter 配合使用:
1type Fn = (this: { id: number }, a: string, b: number) => boolean;
2
3type ThisT = ThisParameterType<Fn>; // { id: number }
4type NoThis = OmitThisParameter<Fn>; // (a: string, b: number) => boolean
ReturnType 对重载函数取最后一个签名实测确认:重载函数用 ReturnType 时,得到的是最后一个重载签名的返回类型,不是所有签名的联合。写类型时要注意这个行为。
Promise
| 工具类型 | 作用 |
|---|---|
Awaited<T> | 递归拆开 Promise |
1Awaited<Promise<string>> // string
2Awaited<Promise<Promise<number>>> // number(递归)
3Awaited<string | Promise<number>> // string | number
字符串(TS 4.1+)
| 工具类型 | 作用 |
|---|---|
Uppercase<S> | 转大写 |
Lowercase<S> | 转小写 |
Capitalize<S> | 首字母大写 |
Uncapitalize<S> | 首字母小写 |
组合出项目专用工具
内置的 12 个不够用时,用它们拼。
把某个键变成可选
1type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
思路:先用 Omit 删掉这个键,再用 Partial<Pick> 加回来(这次是可选的)。
把某个键变成必需
1type RequiredBy<T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>;
只改一个键的类型
1type Override<T, K extends keyof T, V> = Omit<T, K> & { [P in K]: V };
Omit & Pick 是"改一个键"的标准套路。
深度变换
1type DeepPartial<T> = {
2 [K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
3};
4
5type DeepReadonly<T> = {
6 readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
7};
至少一个键必需
1type AtLeastOne<T, K extends keyof T = keyof T> = Omit<T, K> &
2 { [P in K]: Required<Pick<T, P>> & Partial<Record<Exclude<K, P>, never>> }[K];
取出值为某类型的键
1type KeysOfType<T, V> = { [K in keyof T]: T[K] extends V ? K : never }[keyof T];
末尾的 [keyof T] 是索引访问,把映射类型的值联合起来——这是"从映射类型提取联合"的标准技巧。
联合转交叉
1type UnionToIntersection<U> =
2 (U extends unknown ? (x: U) => void : never) extends (x: infer I) => void ? I : never;
靠函数参数的逆变位置实现(见 条件类型与 infer)。
查看源码
内置工具类型定义在 lib.es5.d.ts 里。在 TS 7 中这个文件在平台包里:
1# macOS ARM
2node_modules/@typescript/typescript-darwin-arm64/lib/lib.es5.d.ts
搜索 type Partial、type Pick、type Exclude 就能找到。它们都很短(大多 1-3 行),是学习类型编程最好的材料。
学习路径
- 读
lib.es5.d.ts里的Partial、Pick、Exclude、ReturnType - 自己实现一遍,不看源码
- 做 Type Challenges 的 Easy
- 读真实库的类型定义(
zod的z.infer、Prisma的GetPayload)
相关
- 高级类型 —— 这些工具类型的实现原理
- 条件类型与 infer —— 分布式条件类型
- 映射类型 —— 映射类型与修饰符
- 工程化 —— lib 文件在 TS 7 中的位置