TypeScript 高级类型体操

TypeScript 的类型系统是图灵完备的——这意味着你可以在类型层面做几乎任何事。本文将带你走进「类型体操」的世界,从实用角度掌握那些能真正提升开发效率的高级类型技巧。

条件类型

条件类型是类型体操的基石,语法类似三元运算符:

type IsString<T> = T extends string ? true : false;

type A = IsString<'hello'>; // true
type B = IsString<42>;      // false

结合 infer 关键字,你可以从类型中「提取」出子类型:

type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;

type X = UnwrapPromise<Promise<string>>; // string
type Y = UnwrapPromise<number>;           // number

模板字面量类型

TypeScript 4.1 引入的模板字面量类型让我们可以在类型层面拼接字符串:

type EventName<T extends string> = `on${Capitalize<T>}`;

type ClickEvent = EventName<'click'>; // 'onClick'
type FocusEvent = EventName<'focus'>; // 'onFocus'

映射类型实战

最常见的场景是让某个对象的所有属性变为可选或只读:

type DeepReadonly<T> = {
  readonly [K in keyof T]: T[K] extends object
    ? DeepReadonly<T[K]>
    : T[K];
};

interface Config {
  server: { host: string; port: number };
  debug: boolean;
}

type FrozenConfig = DeepReadonly<Config>;
// 所有层级都变为 readonly

实用工具类型手写

理解标准库工具类型的实现能帮你更好地运用它们:

// 从联合类型中排除特定成员
type MyExclude<T, U> = T extends U ? never : T;

// 提取函数返回类型
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

// 将联合类型转为交叉类型
type UnionToIntersection<U> =
  (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never;

总结

类型体操不是炫技,它能:

  • 消除 as any 类型断言
  • 让 IDE 提供更精确的自动补全
  • 在编译期发现更多潜在 bug1

[!NOTE] 熟练掌握这些模式后,你会发现自己花在解决类型报错上的时间减少了 60% 以上。

掌握这些技巧后,你的 TypeScript 代码将更加类型安全,同时保持优雅和可维护。

Footnotes

  1. 根据 TypeScript 官方团队的统计,在 strict 模式下,约有 15% 的运行时错误可以在编译期通过正确的类型定义避免。