TS 将一个联合类型断言为其中一个类型

57 min read

作为一个例子,考虑 SquareRectangle 的联合类型 ShapeSquareRectangle有共同成员 kind,因此 kind 存在于 Shape 中。

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

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

type Shape = Square | Rectangle;

如果你使用类型保护风格的检查(=====!=!==)或者使用具有判断性的属性(在这里是 kind),TypeScript 将会认为你会使用的对象类型一定是拥有特殊字面量的,并且它会为你自动把类型范围变小:

function area(s: Shape) {
  if (s.kind === 'square') {
    // 现在 TypeScript 知道 s 的类型是 Square
    // 所以你现在能安全使用它
    return s.size * s.size;
  } else {
    // 不是一个 square ?因此 TypeScript 将会推算出 s 一定是 Rectangle
    return s.width * s.height;
  }
}

详细的检查

通常,联合类型的成员有一些自己的行为(代码):

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

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

// 有人仅仅是添加了 `Circle` 类型
// 我们可能希望 TypeScript 能在任何被需要的地方抛出错误
interface Circle {
  kind: 'circle';
  radius: number;
}

type Shape = Square | Rectangle | Circle;

一个可能会让你的代码变差的例子:

function area(s: Shape) {
  if (s.kind === 'square') {
    return s.size * s.size;
  } else if (s.kind === 'rectangle') {
    return s.width * s.height;
  }

  // 如果你能让 TypeScript 给你一个错误,这是不是很棒?
}

你可以通过一个简单的向下思想,来确保块中的类型被推断为与 never 类型兼容的类型。例如,你可以添加一个更详细的检查来捕获错误:

function area(s: Shape) {
  if (s.kind === 'square') {
    return s.size * s.size;
  } else if (s.kind === 'rectangle') {
    return s.width * s.height;
  } else {
    // Error: 'Circle' 不能被赋值给 'never'
    const _exhaustiveCheck: never = s;
  }
}

它将强制你添加一种新的条件:

function area(s: Shape) {
  if (s.kind === 'square') {
    return s.size * s.size;
  } else if (s.kind === 'rectangle') {
    return s.width * s.height;
  } else if (s.kind === 'circle') {
    return Math.PI * s.radius ** 2;
  } else {
    // ok
    const _exhaustiveCheck: never = s;
  }
}

Switch

你可以通过 switch 来实现以上例子。

function area(s: Shape) {
  switch (s.kind) {
    case 'square':
      return s.size * s.size;
    case 'rectangle':
      return s.width * s.height;
    case 'circle':
      return Math.PI * s.radius ** 2;
    default:
      const _exhaustiveCheck: never = s;
  }
}