前置知识: TypeScript

TypeScript 理论知识点

12 minIntermediate

类型系统原理、编译流程与结构化类型机制。

y: number; }

interface Coordinate { x: number; y: number; }

const p: Point2D = { x: 1, y: 2 }; const c: Coordinate = p; // 合法:结构相同

function distance(p: Point2D): number { return Math.sqrt(p.x _ p.x + p.y _ p.y); }

distance(c); // 合法:Coordinate 结构兼容 Point2D


结构化类型的子类型规则:如果 S 的结构包含 T 的所有成员(且类型兼容),则 S 是 T 的子类型。这被称为"宽度子类型化"(width subtyping)。

### 标称化类型(Nominal Typing)

标称化类型系统(如 Java、C#、Rust)中,类型兼容性基于类型的显式声明和名称。即使两个类型结构完全相同,如果名称不同,它们也不兼容。

```java
// Java 示例
class Point2D { int x; int y; }
class Coordinate { int x; int y; }

Point2D p = new Coordinate();  // 编译错误:类型不兼容

结构化型的优势与劣势

特性结构化标称化
灵活性高(鸭子类型低(需显式声明)
安全性较低(可能意外兼容)较高(显式声明关系)
重构可能遗漏不兼容编译器捕获所有不兼容
互操作JSON/JS 天然适配需要型转换
表达力侧重”能做什么”侧重”是什么”

在 TypeScript 中模拟标称化

方法一:品牌型(Branded Type)

type Brand<T, B> = T & { __brand: B };

type USD = Brand<number, 'USD'>;
type EUR = Brand<number, 'EUR'>;

function processUSD(amount: USD): void {
  /* ... */
}

const usd = 100 as USD;
const eur = 100 as EUR;

processUSD(usd); // 合法
processUSD(eur); // 类型错误:EUR 不能赋值给 USD

方法二: + 私有属性

class USD {
  private __brand: 'USD' = 'USD';
  constructor(public value: number) {}
}

class EUR {
  private __brand: 'EUR' = 'EUR';
  constructor(public value: number) {}
}

function processUSD(amount: USD): void {
  /* ... */
}

processUSD(new USD(100)); // 合法
processUSD(new EUR(100)); // 类型错误

协变逆变(Covariance and Contravariance)

基本概念

协变逆变描述了泛型型参数在子型关系中的变化方向。

SubSuper 的子型:

  • 协变(Covariant)Container<Sub>Container<Super> 的子型。方向一致。
  • 逆变(Contravariant)Container<Super>Container<Sub> 的子型。方向相反。
  • 不变(Invariant)Container<Sub>Container<Super> 没有子型关系。
  • 双变(Bivariant)Container<Sub>Container<Super> 互为子型。

函数参数的逆变

函数参数在型安全的前提下应该是逆变的:

class Animal {
  name: string = 'animal';
}

class Dog extends Animal {
  breed: string = 'dog';
}

type AnimalHandler = (animal: Animal) => void;
type DogHandler = (dog: Dog) => void;

// DogHandler 可以安全地当作 AnimalHandler 使用吗?
// 不能!因为 AnimalHandler 可能传入任意 Animal(如 Cat)
// 而 DogHandler 期望接收 Dog,可能访问 Dog 特有的属性

const dogHandler: DogHandler = (dog) => console.log(dog.breed);
const animalHandler: AnimalHandler = dogHandler; // 不安全!
animalHandler(new Animal()); // dog.breed 是 undefined

TypeScript 的函数参数默认是双变的(为了实用性),但开启 strictFunctionTypes 后变为逆变

函数返回值的协变

函数返回值是协变的,这是型安全的:

type AnimalFactory = () => Animal;
type DogFactory = () => Dog;

const dogFactory: DogFactory = () => new Dog();
const animalFactory: AnimalFactory = dogFactory; // 安全:Dog 是 Animal 的子类型

TypeScript 中的协变/逆变规则

位置默认行为strictFunctionTypes说明
函数参数双变逆变严格模式下更安全
函数返回值协变协变始终协变
属性(只读)协变协变只读属性安全
属性(可写)不变不变可写属性需双向兼容
数组协变协变数组作为只读容器

条件型中的协变/逆变推断

TypeScript 的 infer 关键字在条件型中遵循协变/逆变规则:

type ReturnTypeOf<T> = T extends (...args: any[]) => infer R ? R : never;
// R 在返回值位置,协变推断

type ParamTypeOf<T> = T extends (arg: infer P) => any ? P : never;
// P 在参数位置,逆变推断

// 多个推断位置时,协变位置取联合类型,逆变位置取交叉类型
type FlattenedReturnType<T> = T extends (...args: any[]) => infer R extends any[]
  ? R[number]
  : never;

协变位置多个推断 -> 联合类型(union)

逆变位置多个推断 -> 交叉型(intersection)

type CovariantInfer<T> = T extends {
  a: infer R;
  b: infer R;
}
  ? R
  : never;

type Result = CovariantInfer<{ a: string; b: number }>;
// string | number(协变位置,取联合类型)

type ContravariantInfer<T> = T extends {
  a: (x: infer R) => void;
  b: (x: infer R) => void;
}
  ? R
  : never;

type Result2 = ContravariantInfer<{
  a: (x: string) => void;
  b: (x: number) => void;
}>;
// string & number(逆变位置,取交叉类型,此处为 never)

型擦除(Type Erasure)

TypeScript 的编译模型

TypeScript 在编译时执行型检查,然后将型信息完全擦除,输出纯 JavaScript 代码。运行时不存在任何型信息。

// TypeScript 源码
interface User {
  name: string;
  age: number;
}

function greet(user: User): string {
  return `Hello, ${user.name}`;
}

const u: User = { name: 'Alice', age: 30 };
console.log(greet(u));

编译输出:

// JavaScript 输出
function greet(user) {
  return `Hello, ${user.name}`;
}
const u = { name: 'Alice', age: 30 };
console.log(greet(u));

所有型标注(interface、type、泛型参数、型断言)在编译后完全消失。

型擦除的影响

  1. 无运行时类型检查

    function isString(value: unknown): value is string {
      return typeof value === 'string'; // 必须手动实现
    }
    // 不能用 value instanceof string 或 typeof value === "String"
  2. 泛型不保留

    class Container<T> {
      constructor(public value: T) {}
    }
    const c = new Container<number>(42);
    // 运行时无法判断 c 的泛型参数是 number
  3. 枚举的特殊处理

    enum Direction {
      Up = 'UP',
      Down = 'DOWN',
    }
    // 枚举会编译为 JavaScript 对象,是少数保留运行时信息的类型构造
  4. 类部分保留

    class Person {
      constructor(public name: string) {}
    }
    // class 声明保留(JavaScript 类),但类型标注擦除
    // instanceof Person 在运行时可用

运行时型信息的替代方案

  1. Zod / io-ts / class-validator — 运行时型验证库

    import { z } from 'zod';
    
    const UserSchema = z.object({
      name: z.string(),
      age: z.number().min(0).max(150),
    });
    
    type User = z.infer<typeof UserSchema>; // 从 schema 推断类型
    
    const result = UserSchema.parse(unknownData); // 运行时验证
  2. 自定义类型守卫

    function isUser(value: unknown): value is User {
      return (
        typeof value === 'object' &&
        value !== null &&
        'name' in value &&
        typeof (value as User).name === 'string'
      );
    }
  3. 装饰器元数据reflect-metadata

    import 'reflect-metadata';
    
    class UserService {
      @Reflect.metadata('design:type', String)
      name: string;
    }

型擦除的设计哲学

TypeScript 选择型擦除的原因:

  1. 与 JavaScript 完全兼容 — 编译输出是标准 JavaScript,可在任何 JS 环境运行
  2. 零运行时开销型检查只在编译时执行,不影响运行时性能
  3. 渐进式采用 — 可以在现有 JavaScript 项目中逐步添加
  4. 工具链友好型信息仅用于编辑器和编译器,不改变运行时行为

这与 Java/C# 的泛型擦除不同。Java 的泛型擦除是在 JVM 层面,而 TypeScript 的型擦除是在编译层面,彻底移除所有型构造。


其他高级型理论

型宽度与深度

TypeScript 的型兼容性采用”宽度”子型化:子型可以比父型有更多属性。

interface Point2D {
  x: number;
  y: number;
}
interface Point3D {
  x: number;
  y: number;
  z: number;
}

const p3: Point3D = { x: 1, y: 2, z: 3 };
const p2: Point2D = p3; // 合法:Point3D 是 Point2D 的超集

这在函数参数中产生了反直觉但正确的行为:

function setPoint(point: Point2D): void {
  /* ... */
}

setPoint({ x: 1, y: 2, z: 3 }); // 合法(多余属性被忽略)
setPoint({ x: 1, y: 2, z: 3 } as Point3D); // 合法

// 但直接传字面量时,多余属性检查会报错
setPoint({ x: 1, y: 2, z: 3 }); // 错误:对象字面量只能指定已知属性

多余属性检查(Excess Property Check)是 TypeScript 的特殊行为,仅在将对象字面量直接赋值给目标型时触发。

型级别的编程

TypeScript 的型系统是灵完备的,可以在型级别进行计算:

type Fibonacci<
  N extends number,
  T extends number[] = [1],
  U extends number[] = [],
> = T['length'] extends N ? U['length'] : Fibonacci<N, [...T, 1], [...U, ...T]>;

type Fib5 = Fibonacci<5>; // 5

型级别的编程应谨慎使用,过度使用会降低编译速度和代码可读性。


理论速查表

概念核心要点关键细节
结构化基于结构而非名称的兼容性可用品牌型模拟标称化
标称化基于名称的兼容性Java/C#/Rust 采用
协变型方向一致函数返回值、只读属性
逆变型方向相反函数参数(strictFunctionTypes)
不变无子型关系可写属性
双变双向兼容TS 函数参数默认行为
型擦除编译后型信息完全移除运行时无型检查
条件型推断协变位置取联合,逆变位置取交叉infer 关键字
品牌模拟标称化type Brand<T, B> = T & { __brand: B }
多余属性检查对象字面量的额外属性检查仅直接赋值时触发