跳到主要内容

常用 Utility Types(工具类型)

下文默认基于 TypeScript 5.x,并假设开启 strict: true

面试速答(30 秒版 TL;DR)

  • Utility Types 是 TypeScript 内置的一批“类型加工工具”,本质上主要由映射类型条件类型infer 组成。
  • 最常用的几组:
    • 对象形状变换:PartialRequiredReadonlyPickOmitRecord
    • 联合过滤:ExcludeExtractNonNullable
    • 函数 / 类推导:ParametersReturnTypeConstructorParametersInstanceType
    • Promise 展开:Awaited
  • 面试别只背名字,最好能说出:
    • 解决什么问题;
    • 底层大概怎么实现;
    • 哪些是浅层处理,哪些容易误用。

心智模型:工具类型不是新语法,而是“类型积木”

所以标准答法通常不是“它们是编译器黑魔法”,而是:

  • 编译器内置了几种很常用的类型模板;
  • 你也可以用同样思路自己写一套业务工具类型。

1. 对象形状变换类

1.1 Partial<T>

把所有属性变成可选:

interface User {
id: string;
name: string;
age: number;
}

type UserPatch = Partial<User>;

适合:

  • 表单草稿;
  • 更新接口的 patch 参数;
  • 局部覆写配置。

底层直觉:

type MyPartial<T> = {
[K in keyof T]?: T[K];
};

注意:它是浅层的,只改第一层属性是否可选,不会递归深入。

1.2 Required<T>

把所有可选属性改为必填:

type FullUser = Required<Partial<User>>;

适合把“输入阶段可缺省”的类型,恢复成“处理阶段必须完整”的类型。

1.3 Readonly<T>

把所有属性变成只读:

type ReadonlyUser = Readonly<User>;

它约束的是类型层面的写操作,不是运行时冻结对象。

1.4 Pick<T, K>Omit<T, K>

type UserPreview = Pick<User, "id" | "name">;
type UserWithoutAge = Omit<User, "age">;

它们适合从大对象里切出视图模型。

面试里可以补一句:

  • Omit 不是编译器底层原语,本质也是在 Pick 的基础上组合出来的。

1.5 Record<K, V>

type UserMap = Record<string, User>;
type StatusLabel = Record<"idle" | "loading" | "done", string>;

适合表达“键到值的映射”。

如果 K 是字面量联合,它还能帮你检查键是否写全。


2. 联合过滤类

2.1 Exclude<T, U>

从联合里剔除满足 U 的成员:

type T = Exclude<"a" | "b" | "c", "a" | "c">; // "b"

底层思路:

type MyExclude<T, U> = T extends U ? never : T;

2.2 Extract<T, U>

保留满足 U 的成员:

type T = Extract<"a" | "b" | "c", "a" | "c">; // "a" | "c"

2.3 NonNullable<T>

去掉 null | undefined

type Name = NonNullable<string | null | undefined>; // string

这是业务里非常高频的一个工具类型,常配合接口返回值做兜底收窄。


3. 函数与类推导类

3.1 Parameters<T>

提取函数参数元组:

function fetchUser(id: string, retry?: number) {
return { id, retry };
}

type FetchUserArgs = Parameters<typeof fetchUser>;
// [id: string, retry?: number | undefined]

3.2 ReturnType<T>

提取函数返回值:

type FetchUserResult = ReturnType<typeof fetchUser>;
// { id: string; retry: number | undefined }

3.3 ConstructorParameters<T>InstanceType<T>

class Person {
constructor(public name: string, public age: number) {}
}

type PersonCtorArgs = ConstructorParameters<typeof Person>;
type PersonInstance = InstanceType<typeof Person>;

这组工具特别适合做工厂函数、依赖注入、类包装器。

底层思路通常依赖 infer

type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

4. Promise 相关:Awaited<T>

type A = Awaited<Promise<string>>; // string
type B = Awaited<Promise<Promise<number>>>; // number

它的价值在于:

  • 不只是拆一层 Promise
  • 对嵌套 Promise 或 Promise-like 也能持续展开。

这在你写异步工具函数、封装请求层时非常常见。


5. 一张表快速归类

工具类型作用高频场景
Partial<T>全部属性可选patch 更新、表单草稿
Required<T>全部属性必填进入核心处理流程前收紧类型
Readonly<T>全部属性只读配置对象、只读输入
Pick<T, K>选择部分字段视图模型、DTO 裁剪
Omit<T, K>排除部分字段隐藏敏感字段、派生展示类型
Record<K, V>键值映射状态字典、配置表
Exclude<T, U>从联合中排除过滤非法状态
Extract<T, U>从联合中提取提取某类事件 / 某类分支
NonNullable<T>去掉空值接口数据收窄
Parameters<T>提取参数元组包装函数、透传参数
ReturnType<T>提取返回值复用函数结果类型
InstanceType<T>提取实例类型类工厂、IOC
Awaited<T>展开 Promise异步返回值推导

6. 高频题标准答法

6.1 Utility Types 的本质是什么

标准口径:

  • 它们是 TypeScript 内置的通用类型模板;
  • 大多数基于映射类型、条件类型和 infer 实现;
  • 目的是减少重复声明,提高类型复用性。

6.2 PartialReadonly 是深层的吗

默认都不是。

它们只处理第一层属性。嵌套对象内部字段是否可选、是否只读,不会自动递归。

6.3 PickOmit 怎么选

  • 明确要哪些字段时,用 Pick
  • 明确不要哪些字段时,用 Omit

如果原始类型会持续演进,很多团队更偏向 Pick,因为它更显式,也更不容易因为新增字段而误带出去。


7. 常见追问

7.1 Record<string, T> 能不能保证对象键只有这些

不能。

string 太宽了,它表示任意字符串键,不是有限集合。

如果你想让键集合受控,应该把 K 写成字面量联合。

7.2 ReturnType 遇到重载怎么办

通常会以最后一个重载签名作为推导基础,所以复杂重载场景下要小心结果是否符合预期。

7.3 为什么很多团队会自己写 DeepPartial

因为内置 Partial 只处理浅层,而真实业务里经常有深层嵌套配置对象,需要递归可选化。


易错点 / 坑

  • 以为 PartialReadonly 是深层处理,结果嵌套对象还是原样。
  • 滥用 Omit,把原始模型和派生模型关系搞得越来越隐晦。
  • Record<string, T> 当成“有限 key 枚举”,实际上它只是在说“任意字符串都能当键”。
  • 遇到 ReturnTypeParameters 推导不对时,不知道看函数是否有重载或泛型约束。

速记要点(可背诵)

  • 工具类型本质是映射类型、条件类型、infer 的现成封装。
  • 对象改形状:PartialRequiredReadonlyPickOmitRecord
  • 联合做过滤:ExcludeExtractNonNullable
  • 函数 / 类 / Promise 做提取:ParametersReturnTypeInstanceTypeAwaited
  • 内置工具类型大多是浅层处理,复杂业务经常需要自定义扩展版。