浅谈一下 TS 的 可辨识联合类型

658 阅读4分钟

本文正在参加「金石计划 . 瓜分6万现金大奖」 ”

前言

本文主要介绍一下 TypeScript 的 Discriminated union type 也就是 可辨识联合类型,关于这种类型的本质,特点,以及作用分别都是什么。

什么是可辨识联合类型

根据这个名字,我们不难得知,可辨识联合类型的本质上也还是一种联合类型,可以说是一种特殊的联合类型。

TS - 官网 可辨识联合类型

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 这个字段。

那么由此我们就能够先引出,可辨识联合类型的三个特点

    1. 它是一个 联合类型
    1. 它是由共同的具有可辨识属性的字段的类型组成的,简单点成为 可辨识 的特点
    1. 类型守卫

看到上面的三个特点,你会奇怪,类型守卫并没有被提到,那么为什么会存在类型守卫这个特点,不要着急,在后面的文章中会讲到这个特点。

可辨识

首先说说一般的 联合类型(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, RectangleCircle, 在这些接口当中,都含有 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; 
	} 
}

这样就利用 switchcase 运算符来实现类型守卫,从而保证 getArea 这个方法能够正确的计算出需要的图形面积。

image.png

如果你加上一个

case "ellipse":
      return Math.PI * s.width * s.height;

那么 TS 的类型检查器就会提醒你

Type '"ellipse"' is not comparable to type '"square" | "rectangle" | "circle"'

这样做的好处就是,在这个方法当中,无法去调用传入属性 Shape 类型里面不存在的属性,能够保证,面积处理的一定是原本对象中含有的图形类型。

那么要是反过来,如果我们要确保 getArea 方法对 Shape 类型的所有属性都进行处理,那么如果新增了一种类型,但是 getArea 这个方法中并没有与之相应的处理,那么会怎么样呢,

image.png

由于新增了一种 switchcase 中没有的 kind 类型,就是说明,一旦传入的属性为 Ellipe 类型,那么将没有返回值,因为不会走入任何一个分支当中,所有 getArea 函数的返回值给出了报错。

那么我们还可以利用 never 来穷举 Shape 类型中的所有属性,来更加优雅的处理这种情况,首先给这个条件增加默认的结果。然后在默认的结果里面给这个值赋予 never 类型,这样编译器就会给出报错。

image.png

这样错误信息会变得更加的直观,方便我们去定位代码的错误位置。

加入默认处理就是为了避免 Shape 新增属性但是方法中没有添加对应的实现,这些就是为了保证我们写出来的代码类型是绝对安全的。

参考

TypeScript 可辨识联合类型 | 全栈修仙之路

Zhihu - 如何评价 TypeScript 最新加入的 Discriminated union type?

TS - 官网 可辨识联合类型