类型编程(Type-Level Programming) 是 TypeScript 中一种高级特性,它允许开发者在类型系统中进行逻辑操作,而无需运行时代码。通过类型编程,在编译阶段对类型进行推断、转换和验证,从而实现更强大的类型检查和代码约束。
1. 什么是类型编程?
(1) 定义
- 类型编程是指在 TypeScript 的类型系统中编写逻辑代码,以生成或操作类型。
- 它类似于运行时代码编程,但操作的对象是类型,而不是值。
(2) 与运行时代码的区别
- 运行时代码:
- 操作的是值(如变量、函数等)。
- 在程序运行时执行。
- 类型编程:
- 操作的是类型。
- 在编译阶段完成,不会影响运行时性能。
示例
// 运行时代码
function add(a: number, b: number): number {
return a + b;
}
// 类型编程
type Add<A extends number, B extends number> = A extends 1
? B extends 1
? 2
: never
: never;
type Result = Add<1, 1>; // 2
在这里:
add
是运行时代码,用于计算两个数字的和。Add
是类型编程,用于在编译阶段推断两个数字类型的和。
2. 类型编程的核心工具
TypeScript 提供了一些强大的工具,用于实现类型编程。以下是常见的工具及其用途:
(1) 条件类型(Conditional Types)
- 条件类型允许根据某种条件动态地选择类型。
- 语法:
T extends U ? X : Y
。
示例:过滤类型
type NonNullable<T> = T extends null | undefined ? never : T;
type Example = NonNullable<string | null | undefined>; // string
在这里:
NonNullable
使用条件类型过滤掉null
和undefined
。
(2) 映射类型(Mapped Types)
- 映射类型允许对现有类型的所有属性进行批量操作。
- 语法:
{ [K in keyof T]: ... }
。
示例:添加只读修饰符
type Readonly<T> = {
readonly [K in keyof T]: T[K];
};
type User = {
name: string;
age: number;
};
type ReadonlyUser = Readonly<User>;
// 结果:
// type ReadonlyUser = {
// readonly name: string;
// readonly age: number;
// };
在这里:
Readonly
使用映射类型为所有属性添加了readonly
修饰符。
(3) 键名重映射(Key Remapping)
- 键名重映射允许在映射类型中动态地修改键名。
- 语法:
[K in keyof T as NewKeyName]: ...
。
示例:添加前缀
type AddPrefix<T, Prefix extends string> = {
[K in keyof T as `${Prefix}${string & K}`]: T[K];
};
type User = {
name: string;
age: number;
};
type PrefixedUser = AddPrefix<User, "user_">;
// 结果:
// type PrefixedUser = {
// user_name: string;
// user_age: number;
// };
在这里:
AddPrefix
使用键名重映射为所有键添加了前缀。
(4) 条件推断(Infer)
infer
关键字用于从类型中提取信息。- 常用于推断函数返回值类型、数组元素类型等。
示例:提取函数返回值类型
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
function add(a: number, b: number): number {
return a + b;
}
type AddReturnType = ReturnType<typeof add>; // number
在这里:
ReturnType
使用infer
提取了函数的返回值类型。
(5) 递归类型
- 类型编程支持递归操作,可以处理嵌套结构。
示例:深度只读
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};
type NestedData = {
user: {
name: string;
age: number;
};
};
type ReadonlyNestedData = DeepReadonly<NestedData>;
// 结果:
// type ReadonlyNestedData = {
// readonly user: {
// readonly name: string;
// readonly age: number;
// };
// };
在这里:
DeepReadonly
使用递归将嵌套对象的所有属性设置为只读。
3. 类型编程的实际应用场景
(1) 类型安全的工具库
- 类型编程可以用于构建类型安全的工具库,例如
lodash
或ramda
的 TypeScript 版本。
示例:Pick 工具类型
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
type User = {
name: string;
age: number;
email: string;
};
type UserInfo = Pick<User, "name" | "email">;
// 结果:
// type UserInfo = {
// name: string;
// email: string;
// };
(2) API 数据建模
- 类型编程可以用于从接口定义中提取或转换类型。
示例:从 API 响应中提取数据类型
type ApiResponse<T> = {
data: T;
error?: string;
};
type UserData = { id: number; name: string };
type ExtractData<T> = T extends ApiResponse<infer D> ? D : never;
type ApiResult = ExtractData<ApiResponse<UserData>>; // { id: number; name: string }
(3) 状态管理
- 类型编程可以用于 Redux 或其他状态管理工具的类型推断。
示例:Redux Action 类型
type Action<T extends string, P = void> = P extends void
? { type: T }
: { type: T; payload: P };
type IncrementAction = Action<"increment", number>;
type ResetAction = Action<"reset">;
// 结果:
// type IncrementAction = { type: "increment"; payload: number };
// type ResetAction = { type: "reset" };
4. 总结
- 类型编程的作用:
- 实现复杂的类型推断和操作。
- 提高代码的类型安全性和可维护性。
- 支持灵活的类型转换和复用。
- 核心工具:
- 条件类型。
- 映射类型。
- 键名重映射。
- 条件推断(
infer
)。 - 递归类型。
- 实际场景:
- 构建类型安全的工具库。
- API 数据建模。
- 状态管理。