Skip to content

受歧视的 unions

到目前为止,我们所看的大多数例子都是围绕着用简单的类型(如 string 、 boolean 和 number )来缩小单个变量。虽然这很常见,但在 JavaScript 中,大多数时候我们要处理的是稍微复杂的结构。 为了激发灵感,让我们想象一下,我们正试图对圆形和方形等形状进行编码。圆记录了它们的半径,方记录了它们的边长。我们将使用一个叫做 kind 的字段来告诉我们正在处理的是哪种形状。这里是定义 Shape 的第一个尝试。

typescript
interface Shape {
  kind: "circle" | "square";
  radius?: number;
  sideLength?: number;
}

注意,我们使用的是字符串字面类型的联合。 "circle " 和 "square " 分别告诉我们应该把这个形状当作一个圆形还是方形。通过使用 "circle" | "square " 而不是 string ,我们可以避免拼写错误的问题。

typescript
function handleShape(shape: Shape) {
  // oops!
  if (shape.kind === "rect") {
    // ...
  }
}

我们可以编写一个 getArea 函数,根据它处理的是圆形还是方形来应用正确的逻辑。我们首先尝试处理圆形。

typescript
function getArea(shape: Shape) {
  return Math.PI * shape.radius ** 2;
}

在 strictNullChecks 下,这给了我们一个错误——这是很恰当的,因为 radius 可能没有被定义。 但是如果我们对 kind 属性进行适当的检查呢?

typescript
function getArea(shape: Shape) {
  if (shape.kind === "circle") {
    return Math.PI * shape.radius ** 2;
  }
}

嗯,TypeScript 仍然不知道该怎么做。我们遇到了一个问题,即我们对我们的值比类型检查器知道的更多。我们可以尝试使用一个非空的断言 ( radius 后面的那个叹号 ! ) 来说明 radius 肯定存在。

typescript
function getArea(shape: Shape) {
  if (shape.kind === "circle") {
    return Math.PI * shape.radius! ** 2;
  }
}

但这感觉并不理想。我们不得不用那些非空的断言对类型检查器声明一个叹号( ! ),以说服它相信 shape.radius 是被定义的,但是如果我们开始移动代码,这些断言就容易出错。此外,在 strictNullChecks 之外,我们也可以意外地访问这些字段(因为在读取这些字段时,可选属性被认为总是存在的)。我们绝对可以做得更好。

Shape 的这种编码的问题是,类型检查器没有办法根据种类属性知道 radius 或 sideLength 是否存在。我们需要把我们知道的东西传达给类型检查器。考虑到这一点,让我们再来定义一下 Shape。

typescript
interface Circle {
  kind: "circle";
  radius: number;
}

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

type Shape = Circle | Square;

在这里,我们正确地将 Shape 分成了两种类型,为 kind 属性设置了不同的值,但是 radius 和 sideLength 在它们各自的类型中被声明为必需的属性。

让我们看看当我们试图访问 Shape 的半径时会发生什么。

typescript
function getArea(shape: Shape) {
  return Math.PI * shape.radius ** 2;
}

就像我们对 Shape 的第一个定义一样,这仍然是一个错误。当半径是可选的时候,我们得到了一个错误(仅在 strictNullChecks 中),因为 TypeScript 无法判断该属性是否存在。现在 Shape 是一个联合体,TypeScript 告诉我们 shape 可能是一个 Square ,而 Square 并没有定义半径 radius 。 这两种解释都是正确的,但只有我们对 Shape 的新编码仍然在 strictNullChecks 之外导致错误。

但是,如果我们再次尝试检查 kind 属性呢?

typescript
function getArea(shape: Shape) {
  if (shape.kind === "circle") {
    // shape: Circle
    return Math.PI * shape.radius ** 2;
  }
}

这就摆脱了错误! 当 union 中的每个类型都包含一个与字面类型相同的属性时,TypeScript 认为这是一个有区别的 union ,并且可以缩小 union 的成员。

在这种情况下, kind 就是那个共同属性(这就是 Shape 的判别属性)。检查 kind 属性是否为 "circle" ,就可以剔除 Shape 中所有没有 "circle" 类型属性的类型。这就把 Shape 的范围缩小到了 Circle 这个类型。

同样的检查方法也适用于 switch 语句。现在我们可以试着编写完整的 getArea ,而不需要任何讨厌的叹号 ! 非空的断言。

typescript
function getArea(shape: Shape) {
  switch (shape.kind) {
    // shape: Circle
    case "circle":
      return Math.PI * shape.radius ** 2;

    // shape: Square
    case "square":
      return shape.sideLength ** 2;
  }
}

这里最重要的是 Shape 的编码。向 TypeScript 传达正确的信息是至关重要的,这个信息就是 Circle 和 Square 实际上是具有特定种类字段的两个独立类型。这样做让我们写出类型安全的 TypeScript 代码,看起来与我们本来要写的 JavaScript 没有区别。从那里,类型系统能够做 "正确 "的事情,并找出我们 switch 语句的每个分支中的类型。

作为一个旁观者,试着玩一玩上面的例子,去掉一些返回关键词。你会发现,类型检查可以帮助避免在 switch 语句中不小心落入不同子句的 bug。

辨证的联合体不仅仅适用于谈论圆形和方形。它们适合于在 JavaScript 中表示任何类型的消息传递方案, 比如在网络上发送消息( client/server 通信),或者在状态管理框架中编码突变。