类型扩展与收缩

理解类型扩展与收缩

在TypeScript中,类型系统的一个重要特性是能够根据代码的上下文自动扩展或收缩变量的类型。这一机制使得TypeScript既能提供强大的类型检查,又能保持一定的灵活性。

类型扩展(Widening)

类型扩展是指TypeScript在某些情况下会将一个变量的类型从更具体的类型"扩展"为更宽泛的类型。这种情况通常发生在:

  1. 使用letvar声明变量时

    typescript 复制代码
    let x = 3;  // 类型被推断为number,而不是字面量类型3
  2. 初始化变量为nullundefined

    typescript 复制代码
    let y = null;  // 类型被推断为any
    y = "hello";   // 合法,因为y的类型被扩展为any
  3. 在对象字面量中

    typescript 复制代码
    const obj = { x: 1 };  // obj.x的类型被推断为number,而不是1

类型收缩(Narrowing)

类型收缩是类型扩展的逆过程,它通过特定的语法或结构将宽泛的类型缩小为更具体的类型。TypeScript提供了多种方式来实现类型收缩:

  1. 类型守卫(Type Guards)

    typescript 复制代码
    function padLeft(padding: number | string, input: string) {
      if (typeof padding === "number") {
        return " ".repeat(padding) + input;  // 这里padding的类型被收缩为number
      }
      return padding + input;  // 这里padding的类型被收缩为string
    }
  2. 真值检查(Truthiness narrowing)

    typescript 复制代码
    function printAll(strs: string | string[] | null) {
      if (strs && typeof strs === "object") {
        for (const s of strs) {  // strs被收缩为string[]
          console.log(s);
        }
      } else if (typeof strs === "string") {
        console.log(strs);
      }
    }
  3. 相等性检查(Equality narrowing)

    typescript 复制代码
    function example(x: string | number, y: string | boolean) {
      if (x === y) {
        // 这里x和y的类型都被收缩为string
        x.toUpperCase();
        y.toLowerCase();
      }
    }
  4. in操作符检查

    typescript 复制代码
    type Fish = { swim: () => void };
    type Bird = { fly: () => void };
    
    function move(animal: Fish | Bird) {
      if ("swim" in animal) {
        return animal.swim();  // animal被收缩为Fish
      }
      return animal.fly();  // animal被收缩为Bird
    }
  5. instanceof检查

    typescript 复制代码
    function logValue(x: Date | string) {
      if (x instanceof Date) {
        console.log(x.toUTCString());  // x被收缩为Date
      } else {
        console.log(x.toUpperCase());  // x被收缩为string
      }
    }
  6. 控制流分析(Control flow analysis)

    typescript 复制代码
    function example() {
      let x: string | number | boolean;
      x = Math.random() < 0.5;
      console.log(x);  // x是boolean
      
      if (Math.random() < 0.5) {
        x = "hello";
        console.log(x);  // x是string
      } else {
        x = 100;
        console.log(x);  // x是number
      }
      return x;  // x是string | number
    }

自定义类型守卫

当内置的类型收缩机制不够用时,可以创建自定义类型守卫函数:

typescript 复制代码
function isFish(pet: Fish | Bird): pet is Fish {
  return (pet as Fish).swim !== undefined;
}

function getSmallPet(): Fish | Bird {
  // ...
}

let pet = getSmallPet();

if (isFish(pet)) {
  pet.swim();  // pet被收缩为Fish
} else {
  pet.fly();  // pet被收缩为Bird
}

类型收缩的注意事项

  1. never类型:当类型收缩到不可能的情况时,TypeScript会使用never类型表示:

    typescript 复制代码
    function getArea(shape: Circle | Square): number {
      switch (shape.kind) {
        case "circle":
          return Math.PI * shape.radius ** 2;
        case "square":
          return shape.sideLength ** 2;
        default:
          const _exhaustiveCheck: never = shape;  // 确保所有情况都被处理
          return _exhaustiveCheck;
      }
    }
  2. 可辨识联合(Discriminated unions):这是一种利用类型收缩的强大模式:

    typescript 复制代码
    interface Circle {
      kind: "circle";
      radius: number;
    }
    
    interface Square {
      kind: "square";
      sideLength: number;
    }
    
    type Shape = Circle | Square;
    
    function getArea(shape: Shape) {
      switch (shape.kind) {
        case "circle":
          return Math.PI * shape.radius ** 2;
        case "square":
          return shape.sideLength ** 2;
      }
    }

理解类型扩展与收缩是掌握TypeScript类型系统的关键。通过合理利用这些特性,可以编写出既安全又灵活的TypeScript代码。