Home
avatar

TheBlockSky

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 高级用法的必经之路。虽然日常开发中不需要写过于复杂的类型,但理解这些工具能让你:

  1. 写出更安全的代码:在编译期捕获更多错误
  2. 提升开发体验:更好的类型提示和自动补全
  3. 抽象能力:用类型表达业务约束
  4. 阅读开源项目:理解主流库的类型定义

记住一个原则:类型是文档,也是测试。好的类型定义能让代码的意图更加清晰。

TypeScript 类型系统 前端 进阶