工具类型模式和实践
概述
工具类型(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';