常用 Utility Types(工具类型)
在业务代码里,我们经常会遇到这种情况:数据结构(类型)已经有了,但在不同场景下需要“变形”。
比如:
- 表单“编辑接口”只允许改一部分字段 → 需要“可选化”
- 创建接口不允许传
id/createdAt→ 需要“剔除某些字段” - 后端返回
T | null | undefined,但你已经做过判空 → 需要“去空” - 需要从函数类型里拿到参数/返回值 → 需要“提取信息”
TypeScript 内置的 Utility Types 就是为这些场景准备的:通过“组合已有类型”,减少重复定义,让类型更贴近真实业务约束。
先记住“它解决什么问题”,再去记名字。大部分工具类型,你用 2~3 次就能形成肌肉记忆。
一、对象类型:改字段修饰/做字段选择
先准备一个示例类型:
type User = {
id: string;
name: string;
email?: string;
createdAt: string;
};
1) Partial<T>:把属性变成“可选”
常见用途:PATCH 更新(只传需要修改的字段)。
type UpdateUserInput = Partial<User>;
// {
// id?: string;
// name?: string;
// email?: string | undefined;
// createdAt?: string;
// }
Partial<User> 只会把 第一层 属性变可选;如果属性里还有对象,内部不会自动变可选。
如果你需要“深度可选”,通常要自定义(并处理数组/函数等边界):
type DeepPartial<T> = {
[K in keyof T]?: T[K] extends (...args: any[]) => any
? T[K]
: T[K] extends readonly any[]
? T[K]
: T[K] extends object
? DeepPartial<T[K]>
: T[K];
};
2) Required<T>:把属性变成“必选”
常见用途:经过校验/默认值填充后,你希望类型层面保证字段齐全。
type UserAfterValidate = Required<User>;
// email?: string 也会变成 email: string | undefined(属性必有,但值可能是 undefined)
Required 让“属性必须存在”,但并不自动去掉 undefined。如果你需要“既必有又不为 undefined”,通常要结合 NonNullable 或者重建字段类型。
3) Readonly<T>:把属性变成只读
常见用途:只读视图、不可变数据、避免误修改参数。
type ReadonlyUser = Readonly<User>;
declare const u: ReadonlyUser;
// u.name = "x" // ❌ 不能赋值
同样是“浅只读”。数组常见写法是 ReadonlyArray<T> 或 readonly T[]。
4) Pick<T, K>:挑选部分字段
常见用途:DTO / ViewModel / 只暴露必要字段。
type UserPreview = Pick<User, "id" | "name">;
5) Omit<T, K>:剔除部分字段
常见用途:创建入参不允许客户端传 id/createdAt 这类“由系统生成”的字段。
type CreateUserInput = Omit<User, "id" | "createdAt">;
你也可以把它理解成:
type OmitLike<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
当 T 是联合类型时,keyof T 只会保留所有成员都共有的 key,导致 Pick/Omit 结果过窄。
这类场景常用“分布式 Omit”:
type DistributiveOmit<T, K extends keyof any> = T extends any ? Omit<T, K> : never;
这背后原理和“分布式条件类型”有关,可以参考:分布式条件类型。
6) Record<K, T>:用一组 key 构造对象类型
常见用途:
- 枚举到配置的映射(字典表)
id -> 实体的索引结构
type Role = "admin" | "editor" | "viewer";
type RoleNameMap = Record<Role, string>;
// { admin: string; editor: string; viewer: string }
如果 key 来自数组/常量,配合 as const 很好用:
const statuses = ["todo", "doing", "done"] as const;
type Status = (typeof statuses)[number];
type StatusColor = Record<Status, string>;
Record<string, T> 表示“任意字符串 key”,并不会限制只能出现某几种 key;如果你想要“只允许这几个 key”,请让 K 是联合字面量类型(如上面的 Status)。
二、联合类型:过滤/取交集/去空
1) Exclude<T, U>:从 T 中排除能赋值给 U 的成员
type A = Exclude<"a" | "b" | "c", "a" | "c">; // "b"
type B = Exclude<string | number | null, null>; // string | number
2) Extract<T, U>:从 T 中提取能赋值给 U 的成员
type A = Extract<"a" | "b" | "c", "a" | "c" | "d">; // "a" | "c"
记忆:Exclude = 做减法;Extract = 做“交集”。
3) NonNullable<T>:去掉 null 和 undefined
type A = NonNullable<string | null | undefined>; // string
常见用法:你做过判空后,想让后续逻辑更干净。
function mustExist<T>(value: T): NonNullable<T> {
if (value === null || value === undefined) throw new Error("value is empty");
return value as NonNullable<T>;
}
三、函数/类:提取参数、返回值、实例类型
1) Parameters<T>:提取函数参数元组
function request(url: string, timeoutMs: number, withCookie?: boolean) {
// ...
}
type RequestArgs = Parameters<typeof request>;
// [url: string, timeoutMs: number, withCookie?: boolean | undefined]
2) ReturnType<T>:提取函数返回值
function parseUser(json: string) {
return JSON.parse(json) as User;
}
type Parsed = ReturnType<typeof parseUser>; // User
Parameters/ReturnType 在处理函数重载时,通常会以最后一个签名为准。重载场景如果你需要更精确的类型,建议把“想要提取的签名”单独抽成函数类型再处理。
3) ConstructorParameters<T>:提取构造函数参数元组
class Point {
constructor(
public x: number,
public y: number,
) {}
}
type PointCtorArgs = ConstructorParameters<typeof Point>; // [x: number, y: number]
4) InstanceType<T>:提取构造函数的实例类型
type PointInstance = InstanceType<typeof Point>; // Point
5) ThisParameterType<T> / OmitThisParameter<T>
当你显式写了 this 参数时(注意:它不算“真实参数”,只是给 TS 用来约束 this):
function sayName(this: User, prefix: string) {
return `${prefix}${this.name}`;
}
type ThisOfSayName = ThisParameterType<typeof sayName>; // User
type SayNameNoThis = OmitThisParameter<typeof sayName>; // (prefix: string) => string
6) ThisType<T>:为对象字面量提供“上下文 this”
ThisType 本身不产生属性,它是一个“标记类型”,常用于描述对象字面量里 methods 的 this 指向。
type Store<S, A> = {
state: S;
actions: A & ThisType<S & A>;
};
type CounterStore = Store<
{ count: number },
{
inc(): void;
reset(): void;
}
>;
const store: CounterStore = {
state: {count: 0},
actions: {
inc() {
this.count += 1;
},
reset() {
this.count = 0;
},
},
};
四、异步与字符串:Awaited + 字符串变换
1) Awaited<T>:得到“await 之后”的类型
type A = Awaited<Promise<number>>; // number
type B = Awaited<Promise<Promise<string>>>; // string
type C = Awaited<string | Promise<string>>; // string
它在封装“返回 Promise 的工具函数”时非常常见。
2) Uppercase / Lowercase / Capitalize / Uncapitalize
常见用途:配合模板字面量类型做字符串规则约束(比如事件名、CSS 变量名、API 路由片段等)。
type Event = "click" | "mouseMove";
type Upper = Uppercase<Event>; // "CLICK" | "MOUSEMOVE"
type Lower = Lowercase<"HELLO">; // "hello"
type Cap = Capitalize<"hello">; // "Hello"
type Uncap = Uncapitalize<"Hello">; // "hello"
五、实战配方:把工具类型“组合起来”
type UserCreateInput = Omit<User, "id" | "createdAt">;
type UserUpdateInput = Partial<Pick<User, "name" | "email">>;
type UserPublicView = Pick<User, "id" | "name">;
type UserIdToUser = Record<User["id"], User>;
再来一个更贴近业务的例子:事件系统的 payload 映射。
type EventName = "login" | "logout";
type PayloadMap = {
login: {userId: string};
logout: {reason?: string};
};
type Handlers = {
[E in EventName]: (payload: PayloadMap[E]) => void;
};
六、常见坑总结
- 浅 vs 深:
Partial/Required/Readonly都是“浅层”工具类型,深层结构要自定义(并考虑数组/函数)。 - 联合对象的 Pick/Omit:可能因为
keyof行为导致结果过窄,必要时用DistributiveOmit。 - Required ≠ 去 undefined:
Required<{a?: string}>会得到{a: string | undefined},属性必有但值仍可能是undefined。 - Record 不限制 key:
Record<string, T>表示“任意字符串 key”,想限制 key,请让K是字面量联合类型。
面试高频问答
1) Utility Types 的核心价值是什么?
把“类型复用”从复制粘贴升级为类型变换:基于已有类型通过组合表达更多业务约束,减少重复与不一致。
2) Partial 和 Pick 有什么区别?
Partial<T>:保留全部字段,只是把字段变成可选。Pick<T, K>:字段数量变少,只保留K指定的字段。
3) Omit 是怎么实现的?为什么它很常用?
典型实现是 Pick<T, Exclude<keyof T, K>>。它适合“创建入参”这类场景:大多数字段能复用,只排除少数系统字段。
4) Exclude/Extract 为什么能对联合类型生效?
它们依赖“分布式条件类型”:对联合类型逐个成员判断,再把结果合并。理解这点也能帮助你看懂很多自定义工具类型。
5) NonNullable<T> 和 strictNullChecks 有什么关系?
NonNullable<T> 负责在类型层面去掉 null/undefined;strictNullChecks 决定 TS 是否把 null/undefined 当成需要显式处理的类型分支。严格模式下它们配合最好用。
6) Required<T> 为什么不能把 undefined 也去掉?
因为“属性是否存在”和“属性值的取值范围”是两件事。Required 只保证属性存在,不改变属性值类型;去掉 undefined 需要你显式重建类型或结合其他工具类型。
7) Awaited<T> 解决了什么痛点?
它把“Promise 嵌套/混合 Promise 与非 Promise”的类型统一展开成最终值类型,封装 async 工具函数时能避免手写复杂的条件类型。
8) Record 和 {[key: string]: T} 有什么区别?
表达能力类似,但 Record<K, T> 更容易让 key 变成“受控的字面量联合类型”,从而得到更精确的约束(例如 Record<"a" | "b", T>)。