跳到主要内容

常用 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 是“浅”的(Shallow)

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>>;
坑:Omit/Pick 遇到“联合对象类型”可能不符合直觉

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 ≠ 去 undefinedRequired<{a?: string}> 会得到 {a: string | undefined},属性必有但值仍可能是 undefined
  • Record 不限制 keyRecord<string, T> 表示“任意字符串 key”,想限制 key,请让 K 是字面量联合类型。

面试高频问答

1) Utility Types 的核心价值是什么?

把“类型复用”从复制粘贴升级为类型变换:基于已有类型通过组合表达更多业务约束,减少重复与不一致。

2) PartialPick 有什么区别?

  • Partial<T>:保留全部字段,只是把字段变成可选。
  • Pick<T, K>:字段数量变少,只保留 K 指定的字段。

3) Omit 是怎么实现的?为什么它很常用?

典型实现是 Pick<T, Exclude<keyof T, K>>。它适合“创建入参”这类场景:大多数字段能复用,只排除少数系统字段

4) Exclude/Extract 为什么能对联合类型生效?

它们依赖“分布式条件类型”:对联合类型逐个成员判断,再把结果合并。理解这点也能帮助你看懂很多自定义工具类型。

5) NonNullable<T>strictNullChecks 有什么关系?

NonNullable<T> 负责在类型层面去掉 null/undefinedstrictNullChecks 决定 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>)。