Appearance
受歧视的 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 通信),或者在状态管理框架中编码突变。