内置工具类型
概述
TypeScript 内置了一组工具类型(Utility Types),它们是对映射类型、条件类型、keyof 等类型原语的封装。理解这些工具类型的实现原理,不仅能正确使用它们,更能为自定义类型工具打下基础。
面试定位:面试中经常要求手写 Partial、Pick 等工具类型的实现,或在给定场景中选择正确的工具类型组合。这类问题考察的是对映射类型和条件类型的掌握程度。
属性修饰类工具类型
Partial<T> — 全部变为可选
// 源码实现
type Partial<T> = {
[P in keyof T]?: T[P];
};
// 使用场景:更新操作的参数
interface User {
name: string;
email: string;
age: number;
}
function updateUser(id: string, updates: Partial<User>) {
// updates 的每个字段都是可选的
}
updateUser("1", { name: "Alice" }); // ✅ 只更新 name
Required<T> — 全部变为必需
// 源码实现
type Required<T> = {
[P in keyof T]-?: T[P];
};
// -? 语法移除可选修饰符
Readonly<T> — 全部变为只读
// 源码实现
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
// 使用场景:不可变状态
function freeze<T extends object>(obj: T): Readonly<T> {
return Object.freeze(obj);
}
注意:Readonly<T> 只是浅层只读。嵌套对象的属性仍然可变。深层只读需要递归实现:
type DeepReadonly<T> = {
readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P];
};
属性选取类工具类型
Pick<T, K> — 选取指定属性
// 源码实现
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
// 使用场景:API 响应只返回部分字段
type UserSummary = Pick<User, "name" | "email">;
// { name: string; email: string }
Omit<T, K> — 排除指定属性
// 源码实现
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
// 使用场景:创建实体时排除自动生成的字段
type CreateUserInput = Omit<User, "id" | "createdAt">;
Pick vs Omit 的选择:当需要保留的属性少于排除的属性时用 Pick;反之用 Omit。更重要的是可维护性——如果原类型新增了字段:
- Pick 不会自动包含新字段(安全但可能遗漏)
- Omit 会自动包含新字段(便利但可能泄漏)
键值映射类工具类型
Record<K, V> — 构造键值对类型
// 源码实现
type Record<K extends keyof any, T> = {
[P in K]: T;
};
// 使用场景一:字典/映射
type RolePermissions = Record<"admin" | "editor" | "viewer", string[]>;
// 使用场景二:索引签名的类型安全替代
const cache: Record<string, unknown> = {};
// 使用场景三:确保枚举值完整覆盖
type Status = "idle" | "loading" | "error" | "success";
const statusMessages: Record<Status, string> = {
idle: "等待中",
loading: "加载中",
error: "出错了",
success: "完成",
// 缺少任何一个 Status 值都会报错
};
联合类型操作工具类型
Exclude<T, U> — 从联合类型中排除
// 源码实现
type Exclude<T, U> = T extends U ? never : T;
type T = Exclude<"a" | "b" | "c", "a" | "b">;
// "c"
Extract<T, U> — 从联合类型中提取
// 源码实现
type Extract<T, U> = T extends U ? T : never;
type T = Extract<string | number | boolean, string | number>;
// string | number
NonNullable<T> — 排除 null 和 undefined
// 源码实现
type NonNullable<T> = T & {};
// 早期版本:T extends null | undefined ? never : T
type T = NonNullable<string | null | undefined>;
// string
函数相关工具类型
ReturnType<T> — 提取返回值类型
// 源码实现
type ReturnType<T extends (...args: any) => any> =
T extends (...args: any) => infer R ? R : any;
// 使用场景
function createUser() {
return { id: "1", name: "Alice", role: "admin" as const };
}
type User = ReturnType<typeof createUser>;
// { id: string; name: string; role: "admin" }
Parameters<T> — 提取参数类型
// 源码实现
type Parameters<T extends (...args: any) => any> =
T extends (...args: infer P) => any ? P : never;
function search(query: string, limit: number, offset: number) { /* ... */ }
type SearchParams = Parameters<typeof search>;
// [query: string, limit: number, offset: number]
ConstructorParameters<T> 与 InstanceType<T>
class UserService {
constructor(private db: Database, private logger: Logger) {}
}
type Deps = ConstructorParameters<typeof UserService>;
// [db: Database, logger: Logger]
type Instance = InstanceType<typeof UserService>;
// UserService
Promise 相关
Awaited<T>(TypeScript 4.5+)
递归解包 Promise 类型:
// 简化的源码逻辑
type Awaited<T> = T extends Promise<infer U> ? Awaited<U> : T;
type A = Awaited<Promise<string>>; // string
type B = Awaited<Promise<Promise<number>>>; // number
type C = Awaited<string>; // string
字符串操作工具类型
TypeScript 4.1+ 引入了模板字面量类型配套的字符串操作类型:
type Upper = Uppercase<"hello">; // "HELLO"
type Lower = Lowercase<"HELLO">; // "hello"
type Cap = Capitalize<"hello">; // "Hello"
type Uncap = Uncapitalize<"Hello">; // "hello"
工具类型组合模式
实际项目中,工具类型的价值在于组合:
// 模式一:创建输入类型
type CreateInput<T> = Omit<T, "id" | "createdAt" | "updatedAt">;
type UpdateInput<T> = Partial<CreateInput<T>> & { id: string };
// 模式二:使部分属性可选
type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
// 模式三:使部分属性必需
type RequiredBy<T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>;
// 模式四:深层 Partial
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};
// 模式五:提取函数参数中某个位置的类型
type FirstArg<T extends (...args: any) => any> = Parameters<T>[0];
面试高频问题
Q: 请手写 Pick 的实现
回答:
type MyPick<T, K extends keyof T> = {
[P in K]: T[P];
};
关键点:K extends keyof T 确保只能选取 T 上存在的属性;[P in K] 是映射类型语法,遍历联合类型 K 的每个成员。
Q: Omit 为什么用 keyof any 而不是 keyof T?
回答要点:Omit<T, K> 的 K 约束为 keyof any(即 string | number | symbol)而非 keyof T,这是有意的设计选择。它允许排除 T 上不存在的键,使得 Omit 在泛型上下文中更加灵活——当 T 是泛型参数时,编译器可能无法确定 K 是否属于 keyof T。
Q: Record<string, unknown> 和 object 有什么区别?
回答要点:Record<string, unknown> 表示一个具有字符串索引签名的对象,所有值类型为 unknown,可以安全地进行属性访问。object 只表示"非原始类型",不提供任何属性信息,无法进行索引访问。在需要表示"任意键值对对象"时,Record<string, unknown> 是类型安全的选择。