本文正在参加「金石计划 . 瓜分6万现金大奖」 ”
前言
本文主要介绍一下 TypeScript 的 Discriminated union type 也就是 可辨识联合类型,关于这种类型的本质,特点,以及作用分别都是什么。
什么是可辨识联合类型
根据这个名字,我们不难得知,可辨识联合类型的本质上也还是一种联合类型,可以说是一种特殊的联合类型。
The closest equivalent to data is a union of types with discriminant properties, normally called discriminated unions in TypeScript
这是官网上对可辨识联合类型的解释,通俗一点的翻译过来就是,与数据关联的共同字段具有辨别属性的类型的联合,在 TypeScript 中通常称为可辩别联合。
用官方的代码举一个很简单的例子:
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; x: number }
| { kind: "triangle"; x: number; y: number };
那么类型 Shape 就是一个可辨识联合类型,其中的共同的具有辨别属性的字段就是 kind 这个字段。
那么由此我们就能够先引出,可辨识联合类型的三个特点
-
- 它是一个 联合类型
-
- 它是由共同的具有可辨识属性的字段的类型组成的,简单点成为 可辨识 的特点
-
- 类型守卫
看到上面的三个特点,你会奇怪,类型守卫并没有被提到,那么为什么会存在类型守卫这个特点,不要着急,在后面的文章中会讲到这个特点。
可辨识
首先说说一般的 联合类型(Union Type)
type A = string | number
这样的一个类型被使用的时候,我们只能够通过 typeof 或者 instanceof 来检查,但是这样会存在很多不适用的情况,所以说,可辨识联合类型的出现,就是为了解决这种情况,来对多个类型的联合做进一步的类型缩窄。
可辨识联合类型和联合类型不同的地方在于:可辨识联合类型会有一个独特的 tag 由于进行类型收窄,这些 tag 也不是一般的 string,而是单例类型属性,举个例子:
interface Square{
kind: "square";
size: number;
}
interface Rectangle {
kind: "rectangle";
width: number;
height: number;
}
interface Circle {
kind: "circle";
radius: number;
}
在上面的代码当中定义了三个接口 Square, Rectangle 和 Circle, 在这些接口当中,都含有 kind 这个属性,这个属性就是我们上面所说的可辨识的属性。
联合类型
基于前面定义的三个接口,就会发现,其实三个接口的联合类型,就是上面提到的官方的例子:
type Shape = Square | Rectangle | Circle
那么在得到了这个联合类型,并且在我们知道每个元素中都存在这一个独特的 tag 的时候,我们就可以利用这个 tag 以及 TS Type safe 的特点来进行一些类型守卫的操作
类型守卫
定义一个 getArea 的方法,传入三种不同的形状,并且求面积
function getArea(s: Shape){
...
}
根据三种不同形状,存在这不同的求面积的方法,我们可以写出 switch case 的判断:
function getArea(s: Shape): number {
switch (s.kind) {
case "square":
return s.size * s.size;
case "rectangle":
return s.width * s.height;
case "circle":
return Math.PI * s.radius * s.radius;
}
}
这样就利用 switch 和 case 运算符来实现类型守卫,从而保证 getArea 这个方法能够正确的计算出需要的图形面积。
如果你加上一个
case "ellipse":
return Math.PI * s.width * s.height;
那么 TS 的类型检查器就会提醒你
Type '"ellipse"' is not comparable to type '"square" | "rectangle" | "circle"'
这样做的好处就是,在这个方法当中,无法去调用传入属性 Shape 类型里面不存在的属性,能够保证,面积处理的一定是原本对象中含有的图形类型。
那么要是反过来,如果我们要确保 getArea 方法对 Shape 类型的所有属性都进行处理,那么如果新增了一种类型,但是 getArea 这个方法中并没有与之相应的处理,那么会怎么样呢,
由于新增了一种 switch 和 case 中没有的 kind 类型,就是说明,一旦传入的属性为 Ellipe 类型,那么将没有返回值,因为不会走入任何一个分支当中,所有 getArea 函数的返回值给出了报错。
那么我们还可以利用 never 来穷举 Shape 类型中的所有属性,来更加优雅的处理这种情况,首先给这个条件增加默认的结果。然后在默认的结果里面给这个值赋予 never 类型,这样编译器就会给出报错。
这样错误信息会变得更加的直观,方便我们去定位代码的错误位置。
加入默认处理就是为了避免 Shape 新增属性但是方法中没有添加对应的实现,这些就是为了保证我们写出来的代码类型是绝对安全的。