TypeScript 类型体操入门
什么是类型体操?
TypeScript 的类型系统是图灵完备的——这意味着你可以在类型层面进行编程。类型体操(Type Gymnastics)指的是利用 TypeScript 的类型系统,通过泛型、条件类型、映射类型等工具,在编译期完成复杂的类型计算和变换。
// 一个简单的类型体操示例
type ReturnOf<T> = T extends (...args: any[]) => infer R ? R : never;
// 使用
type Fn = (x: number) => string;
type Result = ReturnOf<Fn>; // string基础工具
泛型基础
泛型是类型体操的基石:
// 基础泛型
type Identity<T> = T;
// 多参数泛型
type Pair<T, U> = { first: T; second: U };
// 泛型约束
type Lengthwise<T extends { length: number }> = T['length'];
type StrLen = Lengthwise<string>; // number
type ArrLen = Lengthwise<number[]>; // number条件类型
条件类型允许我们根据条件选择不同的类型:
type IsString<T> = T extends string ? true : false;
type A = IsString<'hello'>; // true
type B = IsString<42>; // false
// 多层条件
type TypeName<T> =
T extends string ? 'string' :
T extends number ? 'number' :
T extends boolean ? 'boolean' :
T extends undefined ? 'undefined' :
T extends null ? 'null' :
'object';
type T1 = TypeName<string>; // 'string'
type T2 = TypeName<true>; // 'boolean'类型推断
使用 infer 关键字在条件类型中推断类型:
// 提取数组元素类型
type ElementType<T> = T extends (infer U)[] ? U : never;
type A = ElementType<number[]>; // number
type B = ElementType<string[][]>; // string[]
// 提取函数参数和返回值类型
type Params<T> = T extends (...args: infer P) => any ? P : never;
type Return<T> = T extends (...args: any[]) => infer R ? R : never;
type Fn = (a: string, b: number) => boolean;
type P = Params<Fn>; // [string, number]
type R = Return<Fn>; // boolean映射类型
映射类型让我们可以遍历联合类型或对象类型的键:
// 基础映射
type Readonly<T> = {
readonly [K in keyof T]: T[K];
};
type Partial<T> = {
[K in keyof T]?: T[K];
};
// 使用 as 子句重映射键
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
interface Person {
name: string;
age: number;
}
type PersonGetters = Getters<Person>;
// { getName: () => string; getAge: () => number }内置映射类型
TypeScript 内置了许多内置的映射类型工具:
// 选取部分属性
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
// 排除部分属性
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
// 提取与排除
type Extract<T, U> = T extends U ? T : never;
type Exclude<T, U> = T extends U ? never : T;
// 实际应用
interface User {
id: number;
name: string;
email: string;
password: string;
}
type PublicUser = Omit<User, 'password'>;
// { id: number; name: string; email: string }
type UserPreview = Pick<User, 'id' | 'name'>;
// { id: number; name: string }实战挑战
挑战 1:实现 DeepReadonly
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object
? T[K] extends Function
? T[K]
: DeepReadonly<T[K]>
: T[K];
};
interface Config {
api: {
url: string;
timeout: number;
};
debug: boolean;
}
type ReadonlyConfig = DeepReadonly<Config>;
// 所有层级都变为 readonly挑战 2:实现 TupleToUnion
type TupleToUnion<T extends any[]> = T[number];
type Tuple = [string, number, boolean];
type Union = TupleToUnion<Tuple>;
// string | number | boolean
// 进阶:递归展开嵌套数组
type Flatten<T extends any[]> = T extends [infer F, ...infer R]
? F extends any[]
? [...Flatten<F>, ...Flatten<R>]
: [F, ...Flatten<R>]
: [];
type Nested = [[1, 2], [3, [4, 5]]];
type Flat = Flatten<Nested>;
// [1, 2, 3, 4, 5]挑战 3:实现 Chainable
type Chainable<T = {}> = {
option<K extends string, V>(
key: K extends keyof T ? never : K,
value: V
): Chainable<T & Record<K, V>>;
get(): T;
};
declare const config: Chainable;
const result = config
.option('name', 'foo')
.option('count', 42)
.get();
// result: { name: string; count: number }挑战 4:实现 Promise.all 类型
declare function PromiseAll<T extends any[]>(
values: readonly [...T]
): Promise<{
[K in keyof T]: T[K] extends Promise<infer R> ? R : T[K];
}>;
// 使用
const p1 = Promise.resolve(1);
const p2 = Promise.resolve('hello');
const p3 = Promise.resolve(true);
const result = await PromiseAll([p1, p2, p3]);
// result: [number, string, boolean]实用技巧
1. 字符串模板字面量类型
type EventName<T extends string> = `on${Capitalize<T>}`;
type ClickEvent = EventName<'click'>; // 'onClick'
type FocusEvent = EventName<'focus'>; // 'onFocus'
// 路由参数提取
type ExtractParams<T extends string> =
T extends `${string}:${infer Param}/${infer Rest}`
? Param | ExtractParams<Rest>
: T extends `${string}:${infer Param}`
? Param
: never;
type Route = '/user/:id/post/:postId';
type Params = ExtractParams<Route>;
// 'id' | 'postId'2. 递归类型
// JSON 类型定义
type JSONValue =
| string
| number
| boolean
| null
| JSONValue[]
| { [key: string]: JSONValue };
// 深层 Partial
type DeepPartial<T> = {
[K in keyof T]?: T[K] extends object
? DeepPartial<T[K]>
: T[K];
};3. 条件类型中的类型分发
// 联合类型在条件类型中会自动分发
type ToArray<T> = T extends any ? T[] : never;
type Result = ToArray<string | number>;
// string[] | number[] (不是 (string | number)[])
// 禁用分发
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;
type Result2 = ToArrayNonDist<string | number>;
// (string | number)[]总结
类型体操是掌握 TypeScript 高级用法的必经之路。虽然日常开发中不需要写过于复杂的类型,但理解这些工具能让你:
- 写出更安全的代码:在编译期捕获更多错误
- 提升开发体验:更好的类型提示和自动补全
- 抽象能力:用类型表达业务约束
- 阅读开源项目:理解主流库的类型定义
记住一个原则:类型是文档,也是测试。好的类型定义能让代码的意图更加清晰。