Skip to content

自定义工具类型

概述

虽然 TypeScript 提供了丰富的内置工具类型,但在实际开发中,我们经常需要创建符合特定业务需求的自定义工具类型。自定义工具类型基于映射类型和条件类型实现,可以帮助我们解决特定的类型问题,提高代码的类型安全性和可维护性。掌握自定义工具类型的创建方法,是成为 TypeScript 高级开发者的重要技能。

提示

创建自定义工具类型需要理解映射类型条件类型的基础概念。建议先学习这些内容,再阅读本章节。

为什么需要自定义工具类型

内置工具类型虽然强大,但有时无法满足特定需求:

typescript
// 场景 1:需要深度只读类型(内置 Readonly 只影响第一层)
interface User {
  name: string;
  address: {
    city: string;
    street: string;
  };
}

type ReadonlyUser = Readonly<User>;
// address 属性仍然可以修改

// 需要自定义深度只读类型
type DeepReadonly<T> = {
  readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P];
};

// 场景 2:需要提取函数类型的参数名
// 内置工具类型无法直接获取参数名

// 场景 3:需要根据条件选择不同的类型转换
// 需要自定义条件逻辑

自定义工具类型让我们能够:

  1. 解决特定业务问题:针对项目中的特定需求创建类型工具
  2. 提高代码复用性:将常用的类型转换逻辑封装为工具类型
  3. 增强类型安全:创建更精确的类型约束
  4. 简化复杂类型:将复杂的类型定义分解为可复用的工具类型

基础自定义工具类型

深度只读类型(DeepReadonly)

内置的 Readonly 只影响第一层属性,对于嵌套对象需要使用递归类型:

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;
  age: number;
  address: {
    city: string;
    street: string;
    coordinates: {
      lat: number;
      lng: number;
    };
  };
  getInfo(): string;
}

type ReadonlyUser = DeepReadonly<User>;
// 类型:{
//   readonly name: string;
//   readonly age: number;
//   readonly address: {
//     readonly city: string;
//     readonly street: string;
//     readonly coordinates: {
//       readonly lat: number;
//       readonly lng: number;
//     };
//   };
//   readonly getInfo: () => string;
// }

const user: ReadonlyUser = {
  name: "John",
  age: 30,
  address: {
    city: "Beijing",
    street: "Main St",
    coordinates: { lat: 39.9, lng: 116.4 }
  },
  getInfo() { return `${this.name}: ${this.age}`; }
};

// ❌ 错误:所有层级都是只读的
// user.name = "Jane";
// user.address.city = "Shanghai";
// user.address.coordinates.lat = 40.0;

深度可选类型(DeepPartial)

类似地,我们可以创建深度可选类型:

typescript
// 深度可选类型实现
type DeepPartial<T> = {
  [P in keyof T]?: T[P] extends object
    ? T[P] extends Function
      ? T[P]
      : DeepPartial<T[P]>
    : T[P];
};

// 使用示例
interface Config {
  api: {
    baseUrl: string;
    timeout: number;
    retry: {
      maxAttempts: number;
      delay: number;
    };
  };
  cache: {
    enabled: boolean;
    ttl: number;
  };
}

type PartialConfig = DeepPartial<Config>;
// 所有嵌套属性都变为可选

const config: PartialConfig = {
  api: {
    baseUrl: "https://api.example.com"
    // timeout 和 retry 都是可选的
  }
  // cache 也是可选的
};

深度必需类型(DeepRequired)

DeepPartial 相反,创建深度必需类型:

typescript
// 深度必需类型实现
type DeepRequired<T> = {
  [P in keyof T]-?: T[P] extends object
    ? T[P] extends Function
      ? T[P]
      : DeepRequired<T[P]>
    : T[P];
};

// 使用示例
interface OptionalConfig {
  api?: {
    baseUrl?: string;
    timeout?: number;
  };
  cache?: {
    enabled?: boolean;
  };
}

type RequiredConfig = DeepRequired<OptionalConfig>;
// 所有嵌套属性都变为必需

const config: RequiredConfig = {
  api: {
    baseUrl: "https://api.example.com",  // 必需
    timeout: 5000  // 必需
  },
  cache: {
    enabled: true  // 必需
  }
};

属性操作工具类型

提取可选属性键(OptionalKeys)

提取对象类型中所有可选属性的键:

typescript
// 提取可选属性键
type OptionalKeys<T> = {
  [K in keyof T]-?: {} extends Pick<T, K> ? K : never;
}[keyof T];

// 使用示例
interface User {
  name: string;        // 必需
  age?: number;       // 可选
  email: string;      // 必需
  phone?: string;     // 可选
}

type Optional = OptionalKeys<User>;
// 类型:"age" | "phone"

提取必需属性键(RequiredKeys)

提取对象类型中所有必需属性的键:

typescript
// 提取必需属性键
type RequiredKeys<T> = {
  [K in keyof T]-?: {} extends Pick<T, K> ? never : K;
}[keyof T];

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

type Required = RequiredKeys<User>;
// 类型:"name" | "email"

排除函数属性(OmitFunctions)

从对象类型中排除所有函数类型的属性:

typescript
// 排除函数属性
type OmitFunctions<T> = {
  [K in keyof T as T[K] extends Function ? never : K]: T[K];
};

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

type UserData = OmitFunctions<User>;
// 类型:{ name: string; age: number; }

提取函数属性(PickFunctions)

只保留对象类型中的函数属性:

typescript
// 提取函数属性
type PickFunctions<T> = {
  [K in keyof T as T[K] extends Function ? K : never]: T[K];
};

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

type UserMethods = PickFunctions<User>;
// 类型:{
//   getName: () => string;
//   getAge: () => number;
//   updateName: (name: string) => void;
// }

函数类型工具

提取函数参数名(FunctionParameterNames)

虽然无法直接获取参数名,但可以提取参数类型:

typescript
// 提取函数的所有参数类型
type FunctionParameters<T extends (...args: any) => any> = 
  T extends (...args: infer P) => any ? P : never;

// 提取第一个参数类型
type FirstParameter<T extends (...args: any) => any> = 
  T extends (first: infer P, ...args: any[]) => any ? P : never;

// 提取最后一个参数类型
type LastParameter<T extends (...args: any) => any> = 
  T extends (...args: any[]) => infer P ? P : never;

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

type Params = FunctionParameters<typeof createUser>;
// 类型:[string, number, string]

type First = FirstParameter<typeof createUser>;
// 类型:string

type Last = LastParameter<typeof createUser>;
// 类型:string

函数重载类型提取

处理函数重载的情况:

typescript
// 提取所有重载签名的返回类型
type OverloadReturnType<T> = 
  T extends {
    (...args: any[]): infer R1;
    (...args: any[]): infer R2;
    (...args: any[]): infer R3;
    (...args: any[]): infer R4;
    (...args: any[]): infer R5;
  } ? R1 | R2 | R3 | R4 | R5 :
  T extends {
    (...args: any[]): infer R1;
    (...args: any[]): infer R2;
    (...args: any[]): infer R3;
    (...args: any[]): infer R4;
  } ? R1 | R2 | R3 | R4 :
  T extends {
    (...args: any[]): infer R1;
    (...args: any[]): infer R2;
    (...args: any[]): infer R3;
  } ? R1 | R2 | R3 :
  T extends {
    (...args: any[]): infer R1;
    (...args: any[]): infer R2;
  } ? R1 | R2 :
  T extends (...args: any[]) => infer R ? R : any;

类型判断工具

判断是否为数组类型(IsArray)

typescript
// 判断是否为数组类型
type IsArray<T> = T extends any[] ? true : false;

// 使用示例
type Test1 = IsArray<string[]>;    // 类型:true
type Test2 = IsArray<number>;      // 类型:false
type Test3 = IsArray<readonly string[]>;  // 类型:true

判断是否为函数类型(IsFunction)

typescript
// 判断是否为函数类型
type IsFunction<T> = T extends (...args: any[]) => any ? true : false;

// 使用示例
type Test1 = IsFunction<() => void>;      // 类型:true
type Test2 = IsFunction<(x: number) => string>;  // 类型:true
type Test3 = IsFunction<string>;         // 类型:false

判断是否为 Promise 类型(IsPromise)

typescript
// 判断是否为 Promise 类型
type IsPromise<T> = T extends Promise<infer P> ? true : false;

// 使用示例
type Test1 = IsPromise<Promise<string>>;  // 类型:true
type Test2 = IsPromise<string>;          // 类型:false

类型转换工具

将联合类型转换为交叉类型(UnionToIntersection)

typescript
// 联合类型转交叉类型
type UnionToIntersection<U> = 
  (U extends any ? (x: U) => void : never) extends (x: infer I) => void
    ? I
    : never;

// 使用示例
type Union = { a: string } | { b: number } | { c: boolean };
type Intersection = UnionToIntersection<Union>;
// 类型:{ a: string } & { b: number } & { c: boolean }

将元组类型转换为联合类型(TupleToUnion)

typescript
// 元组类型转联合类型
type TupleToUnion<T extends readonly any[]> = T[number];

// 使用示例
type Tuple = [string, number, boolean];
type Union = TupleToUnion<Tuple>;
// 类型:string | number | boolean

将对象类型转换为联合类型(ObjectToUnion)

typescript
// 对象类型转联合类型(值)
type ObjectToUnion<T> = T[keyof T];

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

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

实际应用场景

场景 1:API 响应类型处理

typescript
// 定义 API 响应类型
interface ApiResponse<T> {
  data: T;
  status: number;
  message: string;
  timestamp: number;
}

// 提取数据部分
type ExtractData<T> = T extends ApiResponse<infer D> ? D : never;

// 使用示例
type UserResponse = ApiResponse<{ id: number; name: string }>;
type UserData = ExtractData<UserResponse>;
// 类型:{ id: number; name: string }

// 创建只读的 API 响应类型
type ReadonlyApiResponse<T> = DeepReadonly<ApiResponse<T>>;

场景 2:表单验证类型

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

// 验证错误类型:每个字段可能有错误信息
type ValidationErrors<T> = Partial<Record<keyof T, string>>;

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

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

场景 3:状态管理类型

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

// 创建只读状态类型
type ReadonlyState = DeepReadonly<State>;

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

// 创建选择器函数类型
type Selector<T, R> = (state: T) => R;

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

场景 4:事件处理类型

typescript
// 定义事件类型
type EventMap = {
  click: MouseEvent;
  change: Event;
  submit: SubmitEvent;
  keydown: KeyboardEvent;
};

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

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

高级工具类型组合

条件深度操作

根据条件决定是否进行深度操作:

typescript
// 条件深度只读
type ConditionalDeepReadonly<T, Condition extends boolean> = 
  Condition extends true ? DeepReadonly<T> : Readonly<T>;

// 使用示例
type User = {
  name: string;
  address: { city: string };
};

type ShallowReadonly = ConditionalDeepReadonly<User, false>;
type DeepReadonly = ConditionalDeepReadonly<User, true>;

选择性属性操作

只对特定属性进行转换:

typescript
// 只对指定键进行深度只读
type SelectiveDeepReadonly<T, K extends keyof T> = 
  Omit<T, K> & {
    readonly [P in K]: T[P] extends object
      ? DeepReadonly<T[P]>
      : T[P];
  };

// 使用示例
interface Config {
  api: { baseUrl: string };
  cache: { ttl: number };
  name: string;
}

type PartialReadonly = SelectiveDeepReadonly<Config, "api">;
// api 属性是深度只读,其他属性保持原样

注意事项

注意

  1. 递归深度限制:TypeScript 对递归类型有深度限制(默认约 50 层),过深的递归可能导致类型检查失败。

  2. 性能影响:复杂的自定义工具类型可能影响类型检查性能,特别是在大型项目中。

  3. 类型推断:某些复杂的工具类型可能影响 TypeScript 的类型推断能力,导致需要显式类型注解。

  4. 可读性:过于复杂的工具类型可能降低代码可读性,建议添加详细的注释说明。

  5. 测试:自定义工具类型应该通过实际使用场景进行测试,确保行为符合预期。

最佳实践

  1. 从简单开始:先创建简单的工具类型,逐步增加复杂度。

  2. 复用内置类型:尽量基于内置工具类型构建,而不是从头实现。

  3. 命名清晰:使用清晰、描述性的名称,遵循命名约定(如 DeepOptional 等前缀)。

  4. 添加注释:为复杂的工具类型添加注释,说明其用途和实现原理。

  5. 类型测试:使用类型测试验证工具类型的正确性。

  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 ExpectExtends<T, U> = T extends U ? true : false;

type Test2 = ExpectExtends<{ readonly a: string }, DeepReadonly<{ a: string }>>;
// 检查类型是否满足约束

相关链接

基于 VitePress 构建