TypeScript 实用工具类型深度解析

Created on

TypeScript 提供了一系列内置的工具类型(Utility Types),能让我们更优雅地操作和转换类型,避免大量重复的类型定义。这些工具类型在日常业务开发中非常实用,但很多人对它们一知半解,只会用 PartialPick,其余的从未触碰。

本文将系统梳理最常用的 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 代码更加简洁、安全,减少类型定义的重复,也让类型之间的关系更加清晰。下次定义类型时,先想想是否有现成的工具类型可以复用。