什么是类型体操
TypeScript 的类型系统是图灵完备的,这意味着我们可以在类型层面进行复杂的逻辑运算。所谓"类型体操",是社区对 TypeScript 高级类型编程的一种戏称。掌握类型体操技巧,可以帮助我们创建更精确的类型定义,从而在编译阶段发现更多潜在错误,减少运行时 Bug。本文将从基础的泛型开始,逐步深入到条件类型和映射类型等高级话题。
泛型基础
泛型(Generics)是 TypeScript 类型体操的基石。它允许我们在定义函数、接口或类时使用类型参数,实现代码的复用与类型安全的兼顾。
// 基本泛型函数
function identity<T>(value: T): T {
return value;
}
const num = identity<number>(42); // number
const str = identity('hello'); // string(类型推断)
// 泛型接口
interface ApiResponse<T> {
code: number;
message: string;
data: T;
}
interface User {
id: number;
name: string;
email: string;
}
type UserResponse = ApiResponse<User>;
type UserListResponse = ApiResponse<User[]>;
// 泛型约束
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user: User = { id: 1, name: '张三', email: 'zhangsan@example.com' };
const userName = getProperty(user, 'name'); // string
// getProperty(user, 'age'); // 编译错误:'age' 不在 User 的键中
泛型约束(extends)是一个非常重要的概念。通过 K extends keyof T,我们限制了参数 key 只能是对象 obj 的已知属性名,从而在编译时就避免了访问不存在属性的错误。
条件类型
条件类型(Conditional Types)的语法类似三元运算符,根据类型关系做出选择。它的基本形式为 T extends U ? X : Y,含义是:如果 T 可以赋值给 U,则结果类型为 X,否则为 Y。
// 基本条件类型
type IsString<T> = T extends string ? true : false;
type A = IsString<'hello'>; // true
type B = IsString<42>; // false
// 条件类型与 infer 关键字
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
function fetchUser(): User {
return { id: 1, name: '张三', email: 'zhangsan@example.com' };
}
type FetchUserReturn = ReturnType<typeof fetchUser>; // User
// 提取 Promise 中的值类型
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
type P1 = UnwrapPromise<Promise<string>>; // string
type P2 = UnwrapPromise<number>; // number
// 递归解包嵌套 Promise
type DeepUnwrapPromise<T> = T extends Promise<infer U>
? DeepUnwrapPromise<U>
: T;
type P3 = DeepUnwrapPromise<Promise<Promise<boolean>>>; // boolean
infer 关键字用于在条件类型中"推断"某个位置的类型。它只能出现在 extends 子句中,是实现复杂类型运算的核心工具。
映射类型
映射类型(Mapped Types)允许我们基于已有类型创建新类型,通过遍历已有类型的属性来进行转换。
// 将所有属性变为可选
type MyPartial<T> = {
[K in keyof T]?: T[K];
};
// 将所有属性变为只读
type MyReadonly<T> = {
readonly [K in keyof T]: T[K];
};
// 将所有属性变为必需
type MyRequired<T> = {
[K in keyof T]-?: T[K];
};
// 选取指定属性
type MyPick<T, K extends keyof T> = {
[P in K]: T[P];
};
// 排除指定属性
type MyOmit<T, K extends keyof T> = {
[P in keyof T as P extends K ? never : P]: T[P];
};
// 实际使用示例
interface UserProfile {
id: number;
name: string;
email: string;
avatar: string;
bio: string;
}
type UserBasicInfo = MyPick<UserProfile, 'id' | 'name' | 'avatar'>;
type UserUpdateData = MyPartial<MyOmit<UserProfile, 'id'>>;
实用工具类型实现
理解了上述基础概念后,我们可以实现一些更复杂的工具类型,这些在实际项目中非常有用:
// 深度 Partial
type DeepPartial<T> = {
[K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
};
// 获取对象中值为指定类型的键
type KeysOfType<T, V> = {
[K in keyof T]: T[K] extends V ? K : never;
}[keyof T];
type StringKeys = KeysOfType<UserProfile, string>;
// 'name' | 'email' | 'avatar' | 'bio'
// 将联合类型转为交叉类型
type UnionToIntersection<U> = (
U extends any ? (arg: U) => void : never
) extends (arg: infer I) => void
? I
: never;
// 模板字面量类型
type EventName<T extends string> = `on${Capitalize<T>}`;
type ClickEvent = EventName<'click'>; // 'onClick'
type ChangeEvent = EventName<'change'>; // 'onChange'
学习建议
类型体操的学习需要大量练习。推荐通过 type-challenges 项目进行系统化练习,该项目提供了从简单到困难的各种类型挑战。从简单题目开始,逐步理解每个类型工具的用法和组合方式。在实际项目中,不必追求过于复杂的类型定义,在类型安全和代码可读性之间找到平衡才是最重要的。记住:类型是为了帮助开发者,而不是成为开发的障碍。