TypeScript 实用工具类型深度解析
TypeScript 提供了一系列内置的工具类型(Utility Types),能让我们更优雅地操作和转换类型,避免大量重复的类型定义。这些工具类型在日常业务开发中非常实用,但很多人对它们一知半解,只会用 Partial 和 Pick,其余的从未触碰。
本文将系统梳理最常用的 10 个工具类型,每个类型都配合贴近实际的业务代码示例,帮你真正用好 TypeScript 的类型系统。
Partial<T>
将类型 T 的所有属性变为可选。
使用场景:表单的局部更新、PATCH 接口的请求体。
interface User {
id: number;
name: string;
email: string;
avatar: string;
}
// 更新用户信息时,只需要传入需要修改的字段
function updateUser(id: number, patch: Partial<User>) {
// patch 中的所有字段都是可选的
return fetch(`/api/users/${id}`, {
method: "PATCH",
body: JSON.stringify(patch),
});
}
// 只更新邮箱,不需要传其他字段
updateUser(1, { email: "[email protected]" });
原理:
type Partial<T> = {
[P in keyof T]?: T[P];
};
Required<T>
与 Partial 相反,将类型 T 的所有属性变为必填。
使用场景:配置项有默认值时,内部使用完整配置。
interface Config {
timeout?: number;
retries?: number;
baseUrl?: string;
}
// 用户传入的配置可以是部分的
function createClient(options: Config) {
// 合并默认值后,内部使用完整配置
const config: Required<Config> = {
timeout: options.timeout ?? 5000,
retries: options.retries ?? 3,
baseUrl: options.baseUrl ?? "https://api.example.com",
};
return config;
}
Readonly<T>
将类型 T 的所有属性变为只读,防止运行时意外修改。
使用场景:Redux/Zustand 中的 state、常量配置对象。
interface AppState {
user: User;
theme: "light" | "dark";
}
// 函数接收只读的 state,不允许直接修改
function renderApp(state: Readonly<AppState>) {
// state.theme = 'dark'; // ❌ 编译错误:无法分配到只读属性
console.log(state.theme); // ✅ 只读没问题
}
Pick<T, K>
从类型 T 中挑选出指定的属性 K,构造新类型。
使用场景:列表页只展示部分字段,不需要完整的对象类型。
interface Article {
id: number;
title: string;
content: string;
author: string;
publishedAt: string;
tags: string[];
}
// 文章列表只展示这几个字段
type ArticleListItem = Pick<Article, "id" | "title" | "author" | "publishedAt">;
function renderArticleList(articles: ArticleListItem[]) {
articles.forEach((article) => {
console.log(`${article.title} - ${article.author}`);
// article.content // ❌ 编译错误:类型上不存在 content
});
}
Omit<T, K>
与 Pick 相反,从类型 T 中排除指定的属性 K,构造新类型。
使用场景:创建表单不需要 id,因为 id 由服务端生成。
interface Product {
id: number;
name: string;
price: number;
stock: number;
}
// 创建商品时不传 id
type CreateProductDto = Omit<Product, "id">;
function createProduct(data: CreateProductDto) {
return fetch("/api/products", {
method: "POST",
body: JSON.stringify(data),
});
}
createProduct({ name: "键盘", price: 399, stock: 100 }); // ✅
Record<K, V>
构造一个类型,其属性键为 K,值为 V。
使用场景:映射表、枚举到描述文字的对照表。
type OrderStatus = "pending" | "paid" | "shipped" | "delivered" | "cancelled";
// 每个订单状态对应的中文描述和颜色
const statusConfig: Record<OrderStatus, { label: string; color: string }> = {
pending: { label: "待支付", color: "orange" },
paid: { label: "已支付", color: "blue" },
shipped: { label: "已发货", color: "purple" },
delivered: { label: "已完成", color: "green" },
cancelled: { label: "已取消", color: "gray" },
};
// 新增一个状态时,TypeScript 会提示你必须补全对应配置
Exclude<T, U>
从联合类型 T 中排除可以赋值给 U 的类型。
使用场景:过滤联合类型中不需要的成员。
type AllEvents = "click" | "focus" | "blur" | "keydown" | "keyup";
type KeyboardEvents = "keydown" | "keyup";
// 排除键盘事件,只保留鼠标/焦点事件
type NonKeyboardEvents = Exclude<AllEvents, KeyboardEvents>;
// 结果:'click' | 'focus' | 'blur'
function handleNonKeyboardEvent(event: NonKeyboardEvents) {
console.log(event);
}
Extract<T, U>
与 Exclude 相反,从联合类型 T 中提取可以赋值给 U 的类型。
使用场景:从宽泛的联合类型中精确提取出需要的子集。
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; side: number }
| { kind: "triangle"; base: number; height: number };
// 只提取有 radius 属性的形状类型
type CircleShape = Extract<Shape, { kind: "circle" }>;
// 结果:{ kind: 'circle'; radius: number }
function drawCircle(shape: CircleShape) {
console.log(`画圆,半径:${shape.radius}`);
}
ReturnType<T>
获取函数类型 T 的返回值类型。
使用场景:不想手动定义函数返回值的类型,直接从函数推断。
async function fetchUserInfo(id: number) {
const res = await fetch(`/api/users/${id}`);
return res.json() as Promise<{ id: number; name: string; role: string }>;
}
// 自动推断出 fetchUserInfo 的返回类型,无需手动维护
type UserInfo = Awaited<ReturnType<typeof fetchUserInfo>>;
// 结果:{ id: number; name: string; role: string }
// 当 fetchUserInfo 的返回值改变时,UserInfo 会自动同步
function processUser(user: UserInfo) {
console.log(user.role);
}
Parameters<T>
获取函数类型 T 的参数类型,以元组形式返回。
使用场景:封装函数的高阶包装、日志记录、参数校验。
function createOrder(userId: number, productId: number, quantity: number) {
return fetch("/api/orders", {
method: "POST",
body: JSON.stringify({ userId, productId, quantity }),
});
}
// 获取 createOrder 的参数类型
type CreateOrderParams = Parameters<typeof createOrder>;
// 结果:[userId: number, productId: number, quantity: number]
// 封装一个带日志的版本
function withLog<T extends (...args: any[]) => any>(fn: T) {
return function (...args: Parameters<T>): ReturnType<T> {
console.log(`调用 ${fn.name},参数:`, args);
return fn(...args);
};
}
const createOrderWithLog = withLog(createOrder);
createOrderWithLog(1, 2, 3); // 会打印参数日志
组合使用:真实业务场景
工具类型的威力在于组合使用。以下是一个完整的业务示例:
interface UserProfile {
id: number;
username: string;
email: string;
password: string;
avatar: string;
createdAt: string;
updatedAt: string;
}
// 1. 注册时:排除系统生成的字段,password 必填
type RegisterDto = Omit<UserProfile, "id" | "createdAt" | "updatedAt">;
// 2. 更新资料时:排除敏感字段,其余字段可选
type UpdateProfileDto = Partial<
Omit<UserProfile, "id" | "password" | "createdAt" | "updatedAt">
>;
// 3. 公开展示时:只暴露非敏感字段
type PublicProfile = Pick<
UserProfile,
"id" | "username" | "avatar" | "createdAt"
>;
// 4. 管理员列表:只读,防止被意外修改
type AdminUserList = Readonly<PublicProfile>[];
总结
| 工具类型 | 作用 | 常见场景 |
|---|---|---|
Partial<T> | 所有属性可选 | PATCH 请求体、表单草稿 |
Required<T> | 所有属性必填 | 合并默认值后的内部配置 |
Readonly<T> | 所有属性只读 | Redux state、常量 |
Pick<T, K> | 挑选部分属性 | 列表项类型、DTO |
Omit<T, K> | 排除部分属性 | 创建型 DTO |
Record<K, V> | 键值映射 | 枚举对照表、缓存 |
Exclude<T, U> | 联合类型中排除 | 过滤事件类型 |
Extract<T, U> | 联合类型中提取 | 精确类型收窄 |
ReturnType<T> | 获取函数返回类型 | 从函数推断类型 |
Parameters<T> | 获取函数参数类型 | 高阶函数封装 |
掌握这些工具类型,能让你的 TypeScript 代码更加简洁、安全,减少类型定义的重复,也让类型之间的关系更加清晰。下次定义类型时,先想想是否有现成的工具类型可以复用。