Skip to content

工具类型模式和实践

概述

工具类型(Utility Types)是 TypeScript 类型系统的核心特性之一,它们基于映射类型和条件类型实现,能够帮助我们快速创建新的类型,减少重复代码,提高类型安全性。在实际开发中,掌握工具类型的常见模式和实践技巧,能够让我们更高效地使用类型系统,构建更健壮的应用。

提示

本文档假设你已经熟悉内置工具类型自定义工具类型的基础知识。如果你还没有学习这些内容,建议先阅读相关章节。

工具类型设计原则

原则 1:单一职责

每个工具类型应该只做一件事,保持简单和专注:

typescript
// ✅ 好:每个工具类型职责单一
type Partial<T> = { [P in keyof T]?: T[P] };
type Readonly<T> = { readonly [P in keyof T]: T[P] };

// ❌ 不好:一个工具类型做多件事
type PartialAndReadonly<T> = {
  readonly [P in keyof T]?: T[P];  // 同时做两件事
};

原则 2:可组合性

工具类型应该能够轻松组合使用:

typescript
// ✅ 好:可以灵活组合
type PublicUser = Readonly<Omit<User, 'password' | 'id'>>;

// 如果需要同时做多件事,可以组合多个工具类型
type PartialReadonly<T> = Partial<Readonly<T>>;

原则 3:类型安全

工具类型应该保持或增强类型安全性:

typescript
// ✅ 好:保持类型安全
type Pick<T, K extends keyof T> = {
  [P in K]: T[P];
};

// ❌ 不好:可能丢失类型信息
type UnsafePick<T, K> = {
  [P in K]: any;  // 丢失了原始类型信息
};

原则 4:可读性

工具类型的名称应该清晰表达其用途:

typescript
// ✅ 好:名称清晰
type DeepReadonly<T> = { /* ... */ };
type OptionalKeys<T> = { /* ... */ };

// ❌ 不好:名称不清晰
type DR<T> = { /* ... */ };
type OK<T> = { /* ... */ };

常见工具类型模式

模式 1:属性操作模式

这是最常见的模式,用于操作对象类型的属性。

基础属性操作

typescript
// 模式:遍历所有属性并应用转换
type MakeOptional<T> = {
  [K in keyof T]?: T[K];
};

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

type MakeReadonly<T> = {
  readonly [K in keyof T]: T[K];
};

type MakeMutable<T> = {
  -readonly [K in keyof T]: T[K];
};

选择性属性操作

typescript
// 模式:只对特定属性应用转换
type PartialByKeys<T, K extends keyof T> = 
  Omit<T, K> & Partial<Pick<T, K>>;

// 使用示例
interface User {
  name: string;
  age: number;
  email: string;
  phone: string;
}

type UserWithOptionalContact = PartialByKeys<User, 'email' | 'phone'>;
// 类型:{ name: string; age: number; email?: string; phone?: string; }

条件属性操作

typescript
// 模式:根据条件决定是否应用转换
type ConditionalReadonly<T, Condition extends boolean> = 
  Condition extends true ? Readonly<T> : T;

// 使用示例
type User = { name: string; age: number };
type ReadonlyUser = ConditionalReadonly<User, true>;
type MutableUser = ConditionalReadonly<User, false>;

模式 2:属性过滤模式

根据条件过滤对象的属性。

按类型过滤

typescript
// 模式:保留或排除特定类型的属性
type PickByType<T, U> = {
  [K in keyof T as T[K] extends U ? K : never]: T[K];
};

type OmitByType<T, U> = {
  [K in keyof T as T[K] extends U ? never : K]: T[K];
};

// 使用示例
interface User {
  name: string;
  age: number;
  email: string;
  getName(): string;
  getAge(): number;
}

type StringProps = PickByType<User, string>;
// 类型:{ name: string; email: string; }

type NonFunctionProps = OmitByType<User, Function>;
// 类型:{ name: string; age: number; email: string; }

按键名过滤

typescript
// 模式:根据键名模式过滤
type PickByPattern<T, Pattern extends string> = {
  [K in keyof T as K extends `${Pattern}${string}` ? K : never]: T[K];
};

// 使用示例
interface ApiConfig {
  apiBaseUrl: string;
  apiTimeout: number;
  dbHost: string;
  dbPort: number;
}

type ApiProps = PickByPattern<ApiConfig, 'api'>;
// 类型:{ apiBaseUrl: string; apiTimeout: number; }

模式 3:深度操作模式

对嵌套对象进行递归操作。

深度只读

typescript
// 模式:递归应用只读修饰符
type DeepReadonly<T> = {
  readonly [P in keyof T]: T[P] extends object
    ? T[P] extends Function
      ? T[P]
      : DeepReadonly<T[P]>
    : T[P];
};

// 使用示例
interface User {
  name: string;
  address: {
    city: string;
    street: {
      name: string;
      number: number;
    };
  };
}

type ReadonlyUser = DeepReadonly<User>;
// 所有嵌套层级都是只读的

深度可选

typescript
// 模式:递归应用可选修饰符
type DeepPartial<T> = {
  [P in keyof T]?: T[P] extends object
    ? T[P] extends Function
      ? T[P]
      : DeepPartial<T[P]>
    : T[P];
};

条件深度操作

typescript
// 模式:根据条件决定深度或浅层操作
type DeepOrShallow<T, Deep extends boolean> = 
  Deep extends true ? DeepReadonly<T> : Readonly<T>;

模式 4:类型提取模式

从复杂类型中提取所需的部分。

函数类型提取

typescript
// 模式:提取函数的各个部分
type FunctionParts<T extends (...args: any) => any> = {
  params: Parameters<T>;
  return: ReturnType<T>;
  firstParam: Parameters<T>[0];
  lastParam: Parameters<T> extends [...any[], infer L] ? L : never;
};

// 使用示例
function createUser(name: string, age: number, email: string): User {
  return { name, age, email };
}

type Parts = FunctionParts<typeof createUser>;
// 类型:{
//   params: [string, number, string];
//   return: User;
//   firstParam: string;
//   lastParam: string;
// }

对象类型提取

typescript
// 模式:提取对象的特定部分
type ExtractKeys<T, U> = {
  [K in keyof T]: T[K] extends U ? K : never;
}[keyof T];

// 使用示例
interface User {
  name: string;
  age: number;
  email: string;
  getName(): string;
}

type StringKeys = ExtractKeys<User, string>;
// 类型:"name" | "email"

模式 5:类型转换模式

将一种类型转换为另一种类型。

联合类型转元组

typescript
// 模式:将联合类型转换为元组类型
type UnionToTuple<T> = 
  UnionToIntersection<T extends any ? () => T : never> extends () => infer R
    ? [...UnionToTuple<Exclude<T, R>>, R]
    : [];

type UnionToIntersection<U> = 
  (U extends any ? (x: U) => void : never) extends (x: infer I) => void
    ? I
    : never;

// 使用示例
type Colors = "red" | "green" | "blue";
type ColorTuple = UnionToTuple<Colors>;
// 类型:["red", "green", "blue"]

对象转联合类型

typescript
// 模式:将对象类型转换为联合类型
type ObjectToUnion<T> = T[keyof T];

// 使用示例
type Status = {
  pending: "pending";
  approved: "approved";
  rejected: "rejected";
};

type StatusValues = ObjectToUnion<Status>;
// 类型:"pending" | "approved" | "rejected"

模式 6:条件类型模式

根据条件选择不同的类型。

类型判断

typescript
// 模式:判断类型是否满足条件
type IsArray<T> = T extends any[] ? true : false;
type IsFunction<T> = T extends (...args: any[]) => any ? true : false;
type IsPromise<T> = T extends Promise<infer P> ? true : false;

// 使用示例
type Test1 = IsArray<string[]>;  // 类型:true
type Test2 = IsFunction<() => void>;  // 类型:true
type Test3 = IsPromise<Promise<string>>;  // 类型:true

条件类型转换

typescript
// 模式:根据条件进行类型转换
type UnwrapPromise<T> = T extends Promise<infer P> ? P : T;

// 使用示例
type StringType = UnwrapPromise<Promise<string>>;  // 类型:string
type NumberType = UnwrapPromise<number>;  // 类型:number

实际应用场景

场景 1:API 类型定义

在 API 开发中,经常需要定义请求和响应类型:

typescript
// 基础实体类型
interface User {
  id: number;
  name: string;
  email: string;
  password: string;
  createdAt: Date;
  updatedAt: Date;
}

// 创建请求:排除自动生成的字段
type CreateUserRequest = Omit<User, 'id' | 'createdAt' | 'updatedAt'>;

// 更新请求:排除自动生成的字段,所有字段可选
type UpdateUserRequest = Partial<Omit<User, 'id' | 'createdAt' | 'updatedAt'>>;

// API 响应:排除敏感信息
type UserResponse = Omit<User, 'password'>;

// 列表响应:只包含必要字段
type UserListItem = Pick<User, 'id' | 'name' | 'email'>;

// 使用示例
async function createUser(data: CreateUserRequest): Promise<UserResponse> {
  // 实现
}

async function updateUser(
  id: number, 
  data: UpdateUserRequest
): Promise<UserResponse> {
  // 实现
}

async function getUserList(): Promise<UserListItem[]> {
  // 实现
}

场景 2:表单类型处理

表单开发中需要处理不同的表单状态:

typescript
// 表单字段定义
interface FormFields {
  name: string;
  email: string;
  age: number;
  phone?: string;
}

// 表单状态类型
type FormState<T> = {
  values: T;
  errors: Partial<Record<keyof T, string>>;
  touched: Partial<Record<keyof T, boolean>>;
  isValid: boolean;
  isSubmitting: boolean;
};

// 表单验证类型
type ValidationRules<T> = {
  [K in keyof T]?: (value: T[K]) => string | null;
};

// 使用示例
type UserFormState = FormState<FormFields>;
type UserValidationRules = ValidationRules<FormFields>;

const validationRules: UserValidationRules = {
  name: (value) => value.length < 2 ? "姓名至少2个字符" : null,
  email: (value) => !value.includes("@") ? "邮箱格式不正确" : null,
  age: (value) => value < 18 ? "年龄必须大于18" : null,
};

场景 3:状态管理类型

在状态管理中,需要定义只读状态和更新函数:

typescript
// 状态类型定义
interface AppState {
  user: {
    name: string;
    age: number;
    preferences: {
      theme: "light" | "dark";
      language: string;
    };
  };
  settings: {
    apiUrl: string;
    timeout: number;
  };
}

// 只读状态类型
type ReadonlyState = DeepReadonly<AppState>;

// 状态更新函数类型
type StateUpdater<T> = (state: T) => Partial<T>;

// 选择器函数类型
type Selector<TState, TResult> = (state: TState) => TResult;

// 使用示例
const selectUserName: Selector<AppState, string> = (state) => 
  state.user.name;

const selectUserTheme: Selector<AppState, "light" | "dark"> = (state) => 
  state.user.preferences.theme;

function updateUser(
  updater: StateUpdater<AppState['user']>
): void {
  // 实现状态更新
}

场景 4:事件处理类型

定义类型安全的事件处理系统:

typescript
// 事件映射类型
type EventMap = {
  click: MouseEvent;
  change: Event;
  submit: SubmitEvent;
  keydown: KeyboardEvent;
  focus: FocusEvent;
};

// 事件处理器类型
type EventHandlers<T> = {
  [K in keyof T]: (event: T[K]) => void;
};

// 事件监听器类型
type EventListeners<T> = {
  [K in keyof T]: Array<(event: T[K]) => void>;
};

// 使用示例
const handlers: EventHandlers<EventMap> = {
  click: (e) => console.log("clicked", e.clientX, e.clientY),
  change: (e) => console.log("changed", e.target),
  submit: (e) => console.log("submitted", e.submitter),
  keydown: (e) => console.log("key pressed", e.key),
  focus: (e) => console.log("focused", e.target),
};

// 类型安全的事件发射器
class EventEmitter<T extends Record<string, any>> {
  private listeners: EventListeners<T> = {} as EventListeners<T>;

  on<K extends keyof T>(
    event: K,
    handler: (event: T[K]) => void
  ): void {
    if (!this.listeners[event]) {
      this.listeners[event] = [];
    }
    this.listeners[event].push(handler);
  }

  emit<K extends keyof T>(event: K, data: T[K]): void {
    const handlers = this.listeners[event];
    if (handlers) {
      handlers.forEach(handler => handler(data));
    }
  }
}

场景 5:配置对象类型

处理不同环境的配置:

typescript
// 基础配置类型
interface BaseConfig {
  api?: {
    baseUrl?: string;
    timeout?: number;
    retry?: {
      maxAttempts?: number;
      delay?: number;
    };
  };
  cache?: {
    enabled?: boolean;
    ttl?: number;
  };
  logging?: {
    level?: "debug" | "info" | "warn" | "error";
    file?: string;
  };
}

// 开发环境:所有字段可选
type DevConfig = DeepPartial<BaseConfig>;

// 生产环境:所有字段必需
type ProdConfig = DeepRequired<BaseConfig>;

// 只读配置:防止配置被修改
type ReadonlyConfig<T> = DeepReadonly<T>;

// 配置验证函数
function validateConfig<T extends BaseConfig>(
  config: T
): config is DeepRequired<T> {
  // 验证逻辑
  return true;
}

// 使用示例
const devConfig: DevConfig = {
  api: {
    baseUrl: "http://localhost:3000"
    // 其他字段可选
  }
};

const prodConfig: ProdConfig = {
  api: {
    baseUrl: "https://api.example.com",
    timeout: 5000,
    retry: {
      maxAttempts: 3,
      delay: 1000
    }
  },
  cache: {
    enabled: true,
    ttl: 3600
  },
  logging: {
    level: "error",
    file: "/var/log/app.log"
  }
};

最佳实践

实践 1:优先使用内置工具类型

TypeScript 内置的工具类型经过优化,性能更好,语义更清晰:

typescript
// ✅ 好:使用内置工具类型
type PartialUser = Partial<User>;
type ReadonlyUser = Readonly<User>;

// ❌ 不好:重复实现已有功能
type PartialUser = {
  [K in keyof User]?: User[K];
};

实践 2:合理组合工具类型

合理组合多个工具类型,避免重复定义:

typescript
// ✅ 好:组合使用工具类型
type PublicUser = Readonly<Omit<User, 'password' | 'id'>>;

// ❌ 不好:重复定义类型
interface PublicUser {
  readonly name: string;
  readonly email: string;
  // ...
}

实践 3:使用类型别名提高可读性

为复杂的工具类型组合创建清晰的别名:

typescript
// ✅ 好:使用清晰的类型别名
type CreateUserInput = Omit<User, 'id' | 'createdAt' | 'updatedAt'>;
type UpdateUserInput = Partial<CreateUserInput>;
type UserResponse = Omit<User, 'password'>;

// ❌ 不好:直接使用复杂的工具类型
function createUser(
  data: Omit<User, 'id' | 'createdAt' | 'updatedAt'>
): Omit<User, 'password'> {
  // ...
}

实践 4:添加类型注释

为复杂的工具类型添加注释说明:

typescript
/**
 * 深度只读类型
 * 递归地将对象类型的所有属性(包括嵌套属性)变为只读
 * 注意:函数类型不会被递归处理
 */
type DeepReadonly<T> = {
  readonly [P in keyof T]: T[P] extends object
    ? T[P] extends Function
      ? T[P]
      : DeepReadonly<T[P]>
    : T[P];
};

实践 5:避免过度复杂的工具类型

保持工具类型的简单性,避免过度复杂:

typescript
// ✅ 好:简单清晰
type Optional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;

// ❌ 不好:过度复杂,难以理解
type Complex<T, K, U, V> = 
  T extends K 
    ? U extends V 
      ? { [P in keyof T]: T[P] extends Function ? never : T[P] }
      : never
    : Partial<T>;

实践 6:测试工具类型

验证工具类型的行为是否符合预期:

typescript
// 类型测试工具
type Expect<T extends true> = T;
type Equal<X, Y> = 
  (<T>() => T extends X ? 1 : 2) extends 
   (<T>() => T extends Y ? 1 : 2) ? true : false;

// 测试示例
type Test1 = Expect<Equal<
  DeepReadonly<{ a: { b: string } }>,
  { readonly a: { readonly b: string } }
>>;

type Test2 = Expect<Equal<
  Optional<{ a: string; b: number }, 'a'>,
  { a?: string; b: number }
>>;

常见陷阱和注意事项

陷阱 1:深度限制

TypeScript 对递归类型有深度限制(默认约 50 层):

typescript
// ⚠️ 注意:过深的递归可能导致类型检查失败
type VeryDeepType = {
  level1: {
    level2: {
      // ... 超过 50 层
    };
  };
};

// 解决方案:限制递归深度或使用迭代方式
type DeepReadonly<T, Depth extends number = 5> = 
  Depth extends 0 
    ? T
    : {
        readonly [P in keyof T]: T[P] extends object
          ? T[P] extends Function
            ? T[P]
            : DeepReadonly<T[P], Prev<Depth>>
          : T[P];
      };

陷阱 2:性能影响

复杂的工具类型可能影响类型检查性能:

typescript
// ⚠️ 注意:复杂的工具类型组合可能很慢
type VeryComplexType = DeepReadonly<
  DeepPartial<
    Omit<
      Pick<User, 'name' | 'email'>,
      'name'
    >,
    'email'
  >
>;

// 解决方案:简化类型定义,使用中间类型别名
type UserEmail = Pick<User, 'email'>;
type OptionalEmail = Partial<UserEmail>;
type ReadonlyOptionalEmail = DeepReadonly<OptionalEmail>;

陷阱 3:类型推断失败

某些复杂的工具类型可能影响 TypeScript 的类型推断:

typescript
// ⚠️ 注意:可能影响类型推断
function process<T>(data: DeepReadonly<T>): T {
  return data as T;  // 需要类型断言
}

// 解决方案:提供明确的类型注解
function process<T>(data: DeepReadonly<T>): T {
  return data as unknown as T;
}

陷阱 4:函数类型处理

递归类型需要特别处理函数类型:

typescript
// ✅ 好:正确处理函数类型
type DeepReadonly<T> = {
  readonly [P in keyof T]: T[P] extends object
    ? T[P] extends Function
      ? T[P]  // 函数类型不递归
      : DeepReadonly<T[P]>
    : T[P];
};

// ❌ 不好:函数类型也被递归处理
type BadDeepReadonly<T> = {
  readonly [P in keyof T]: T[P] extends object
    ? DeepReadonly<T[P]>  // 函数类型也会被递归
    : T[P];
};

工具类型库推荐

在实际项目中,可以考虑使用成熟的工具类型库:

type-fest

一个流行的 TypeScript 工具类型集合:

typescript
import type { 
  Simplify,
  MergeExclusive,
  RequireAtLeastOne 
} from 'type-fest';

// 简化交叉类型
type Simplified = Simplify<{ a: string } & { b: number }>;
// 类型:{ a: string; b: number; }

// 至少需要一个属性
type AtLeastOne = RequireAtLeastOne<
  { a?: string; b?: number; c?: boolean },
  'a' | 'b'
>;

utility-types

另一个常用的工具类型库:

typescript
import type {
  DeepPartial,
  DeepReadonly,
  Optional
} from 'utility-types';

相关链接

基于 VitePress 构建