返回首页

说说 TypeScript 中的类型兼容性和类型保护?

问题解析

类型兼容性和类型保护是 TypeScript 类型系统的核心机制,考察候选人对结构类型系统、类型收窄以及各种类型保护手段的理解。

核心概念

  • 类型兼容性:一个类型能否赋值给另一个类型
  • 结构类型系统:基于结构而非名义的类型系统
  • 类型保护:运行时检查收窄类型范围
  • 类型谓词parameter is Type 自定义类型保护
  • 可赋值性:类型之间的赋值规则

详细解答

1. 类型兼容性

TypeScript 使用结构类型系统(Structural Type System),类型兼容性基于类型的结构而非名称。

1.1 基本规则

interface Named {
  name: string;
}

class Person {
  name: string = '';
}

let p: Named;
// ok,因为 Person 的结构满足 Named
p = new Person();

1.2 对象兼容性

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

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

let p2d: Point2D = { x: 0, y: 0 };
let p3d: Point3D = { x: 0, y: 0, z: 0 };

// 可以,Point3D 包含 Point2D 的所有属性
p2d = p3d;

// 不可以,Point2D 缺少 z 属性
// p3d = p2d;  // Error

规则

  • 源类型必须包含目标类型的所有必需属性
  • 额外的属性不影响兼容性

1.3 函数兼容性

let x = (a: number) => 0;
let y = (b: number, s: string) => 0;

y = x;  // ok,忽略额外参数
// x = y;  // Error,缺少参数

参数规则

  • 目标函数的参数可以少于源函数(忽略额外参数)
  • 目标函数的参数不能多于源函数

返回值规则

let x = () => ({ name: 'Alice' });
let y = () => ({ name: 'Alice', location: 'Seattle' });

x = y;  // ok,返回值包含所需属性
// y = x;  // Error,缺少 location 属性

1.4 枚举兼容性

enum Status { Ready, Waiting };
enum Color { Red, Blue, Green };

let status = Status.Ready;
// status = Color.Green;  // Error,不同枚举不兼容

1.5 类兼容性

class Animal {
  feet: number = 0;
  constructor(name: string, numFeet: number) {}
}

class Size {
  feet: number = 0;
  constructor(numFeet: number) {}
}

let a: Animal = new Animal('', 0);
let s: Size = new Size(0);

a = s;  // ok,结构相同
s = a;  // ok,结构相同

注意:类的私有成员和受保护成员会影响兼容性。

2. 类型保护

类型保护是可执行运行时检查的一种表达式,用于确保该类型在一定的范围内。

2.1 typeof 类型保护

function padLeft(value: string, padding: string | number) {
  if (typeof padding === "number") {
    // padding 被收窄为 number 类型
    return Array(padding + 1).join(" ") + value;
  }
  if (typeof padding === "string") {
    // padding 被收窄为 string 类型
    return padding + value;
  }
  throw new Error(`Expected string or number, got '${padding}'.`);
}

支持的类型numberstringbooleansymbolundefinedfunctionobject

2.2 instanceof 类型保护

class Bird {
  fly() {
    console.log('flying');
  }
}

class Fish {
  swim() {
    console.log('swimming');
  }
}

function move(pet: Bird | Fish) {
  if (pet instanceof Bird) {
    pet.fly();  // pet 被收窄为 Bird
  } else {
    pet.swim(); // pet 被收窄为 Fish
  }
}

2.3 in 类型保护

interface Bird {
  fly(): void;
}

interface Fish {
  swim(): void;
}

function move(animal: Bird | Fish) {
  if ("fly" in animal) {
    animal.fly();  // animal 被收窄为 Bird
  } else {
    animal.swim(); // animal 被收窄为 Fish
  }
}

2.4 字面量类型保护

interface Square {
  kind: "square";
  size: number;
}

interface Rectangle {
  kind: "rectangle";
  width: number;
  height: number;
}

type Shape = Square | Rectangle;

function area(s: Shape) {
  switch (s.kind) {
    case "square":
      return s.size * s.size;  // s 被收窄为 Square
    case "rectangle":
      return s.width * s.height;  // s 被收窄为 Rectangle
  }
}

2.5 自定义类型保护(类型谓词)

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

function move(pet: Fish | Bird) {
  if (isFish(pet)) {
    pet.swim();  // pet 被收窄为 Fish
  } else {
    pet.fly();   // pet 被收窄为 Bird
  }
}

类型谓词parameter is Type,返回 boolean 的函数,用于自定义类型保护。

2.6 可辨识联合(Discriminated Unions)

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;
    default:
      // 穷尽检查
      const _exhaustiveCheck: never = shape;
      return _exhaustiveCheck;
  }
}

3. 类型兼容性规则总结

// 1. 基本类型
let str: string = "hello";
// let num: number = str;  // Error,基本类型不兼容

// 2. 对象类型 - 基于结构
interface A { x: number; }
interface B { x: number; y: number; }
let a: A = { x: 1 };
let b: B = { x: 1, y: 2 };
a = b;  // ok
// b = a;  // Error

// 3. 函数参数 - 双变(Bivariant)
let fn1: (x: number) => void = (x) => {};
let fn2: (x: number, y: number) => void = (x, y) => {};
fn2 = fn1;  // ok

// 4. 函数返回值 - 协变(Covariant)
let fn3: () => { x: number; } = () => ({ x: 1 });
let fn4: () => { x: number; y: number; } = () => ({ x: 1, y: 2 });
fn3 = fn4;  // ok

// 5. 数组 - 协变
let arr1: Array<{ x: number; }> = [{ x: 1 }];
let arr2: Array<{ x: number; y: number; }> = [{ x: 1, y: 2 }];
arr1 = arr2;  // ok

深入理解

严格类型检查

{
  "compilerOptions": {
    "strictFunctionTypes": true,  // 严格函数类型检查
    "strictNullChecks": true,     // 严格 null 检查
    "strictPropertyInitialization": true  // 严格属性初始化
  }
}

类型收窄的层次

function example(x: string | number | boolean) {
  if (typeof x === "string") {
    // x: string
    x.toUpperCase();
  } else if (typeof x === "number") {
    // x: number
    x.toFixed(2);
  } else {
    // x: boolean(穷尽)
    x.valueOf();
  }
}

最佳实践

  1. 利用类型保护收窄类型:避免使用类型断言
  2. 使用自定义类型保护:提高代码可读性和复用性
  3. 设计可辨识联合:使用 kind 等字面量属性区分类型
  4. 穷尽检查:在 switch 中使用 never 确保所有情况都被处理
  5. 开启严格模式strict: true 获得更强的类型检查

面试要点

  1. 类型兼容性:基于结构而非名称,源类型必须包含目标类型的所有必需属性
  2. 对象兼容性:属性多的可以赋值给属性少的(协变)
  3. 函数兼容性:参数少的可以赋值给参数多的(逆变,双变)
  4. 类型保护方式
    • typeof:基本类型检查
    • instanceof:类实例检查
    • in:属性存在检查
    • 字面量检查:可辨识联合
    • 自定义类型保护:is 类型谓词
  5. 可辨识联合:使用共同的可辨识属性(如 kind)区分联合类型成员
  6. 穷尽检查:使用 never 确保 switch 处理所有情况
  7. 严格类型检查strictFunctionTypesstrictNullChecks 等配置