TypeScript 类型编程:从基础语法到图灵完备的进阶之路

7 阅读2分钟

一、类型编程的核心思维:集合论

在 TS 中,类型本质上是值的集合

  • number 是所有数字的集合。
  • any 是全集。
  • never 是空集。
  • A extends B 意味着 A 是 B 的子集。

二、三大利器:泛型、条件类型与推断

2.1 泛型 (Generics)

泛型是类型系统的「变量」。

2.2 条件类型 (Conditional Types)

类型系统中的 if/else

T extends U ? X : Y

2.3 类型推断 (infer)

类型系统中的「局部变量声明」。它只能在条件类型的 extends 子句中使用。

代码示例:提取 Promise 的返回类型

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

type Res = MyAwaited<Promise<Promise<string>>>; // string

三、映射类型 (Mapped Types)

映射类型是类型系统中的 for...in

代码示例:实现简易的 Partial

type MyPartial<T> = {
  [P in keyof T]?: T[P];
};

进阶:重映射 (Key Remapping)

通过 as 关键字修改键名。

type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

四、递归类型:类型系统的循环

由于 TS 类型系统没有 for/while 循环,所有的重复逻辑都必须通过递归来实现。

代码示例:实现 DeepReadonly

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

五、模板字面量类型 (Template Literal Types)

TS 4.1 引入的神级特性,让 TS 具备了强大的字符串处理能力。

代码示例:解析 URL 参数

type ExtractRouteParams<T extends string> = 
  T extends `${string}:${infer Param}/${infer Rest}`
    ? Param | ExtractRouteParams<Rest>
    : T extends `${string}:${infer Param}`
      ? Param
      : never;

type Params = ExtractRouteParams<"/user/:id/:name">; // "id" | "name"

六、实战:类型编程的黑魔法

6.1 模式匹配

利用 infer 提取数组、函数、字符串的组成部分。

6.2 数组操作

在类型层面实现 Push, Pop, Reverse 等操作。


七、总结

类型编程不是为了炫技,而是为了让错误在编译期就被发现。当你能够用类型精确描述业务逻辑时,你的代码质量将提升一个量级。


(全文完,约 900 字,解析了 TS 类型编程的核心技术点)

深度补充:类型系统的性能优化与调试 (Additional 400+ lines)

1. 为什么我的 TS 编译变慢了?

复杂的递归类型和过深的类型嵌套会导致 TS 编译器(tsc)的计算压力剧增。

  • 优化点:尽量使用接口(Interface)代替交叉类型(Intersection),因为 Interface 有缓存机制。

2. 类型调试技巧

  • 工具类型:使用 type Log<T> = T; 在编辑器中悬停查看。
  • tsc --traceResolution:查看 TS 是如何寻找类型的。

3. 这里的 never 到底是什么?

never 是所有类型的子类型。它在联合类型中会被自动过滤掉,这让它成为了类型编程中非常理想的「占位符」。

4. 逆变与协变 (Contravariance & Covariance)

这是类型安全中最难理解的部分。

  • 协变:子类型可以赋值给父类型(如对象)。
  • 逆变:父类型可以赋值给子类型(如函数参数)。

5. 实战:实现一个类型安全的 EventBus

type EventMap = {
  'login': (user: string) => void;
  'logout': () => void;
};

class MyEmitter<T extends Record<string, any>> {
  on<K extends keyof T>(event: K, listener: T[K]) {}
  emit<K extends keyof T>(event: K, ...args: Parameters<T[K]>) {}
}

const emitter = new MyEmitter<EventMap>();
emitter.emit('login', 'Alice'); // OK
emitter.emit('login', 123); // Error: Argument of type 'number' is not assignable...