3. 可辨识联合类型
可以把单例类型、联合类型、类型保护和类型别名这几种类型进行合并,来创建一个叫做可辨识联合类型,它也可称作标签联合或代数数据类型。
所谓单例类型,可以理解为符合单例模式的数据类型,比如枚举成员类型,字面量类型。
可辨识联合类型要求具有两个要素:
- 具有普通的单例类型属性。
- 一个类型别名,包含了那些类型的联合。
可辨识联合类型就是为了保证每个case都能被处理。
来看一个例子:
interface Square { kind: "square"; // 具有辨识性的属性 size: number; } interface Rectangle { kind: "rectangle"; // 具有辨识性的属性 height: number; width: number; } interface Circle { kind: "circle"; // 具有辨识性的属性 radius: number; } type Shape = Square | Rectangle | Circle; function getArea(s: Shape) { switch (s.kind) { case "square": return s.size * s.size; case "rectangle": return s.height * s.width; case "circle": return Math.PI * s.radius ** 2; } } 复制代码
上面这个例子中,我们的 Shape 即可辨识联合类型,它是三个接口的联合,而这三个接口都有一个 kind 属性,且每个接口的 kind 属性值都不相同,能够起到标识作用。 函数内应该包含联合类型中每一个接口的 case。
如果函数内没有包含联合类型中每一个接口的 case。希望编译器应该给出提示。有以下两种完整性检查的方法:使用 strictNullChecks和使用 never 类型。
(1)使用 strictNullChecks
对上面的例子加一种接口:
interface Square { kind: "square"; size: number; } interface Rectangle { kind: "rectangle"; height: number; width: number; } interface Circle { kind: "circle"; radius: number; } interface Triangle { kind: "triangle"; bottom: number; height: number; } type Shape = Square | Rectangle | Circle | Triangle; function getArea(s: Shape) { switch (s.kind) { case "square": return s.size * s.size; case "rectangle": return s.height * s.width; case "circle": return Math.PI * s.radius ** 2; } } 复制代码
这里,Shape 联合有四种接口,但函数的 switch 里只包含三个 case,这个时候编译器并没有提示任何错误,因为当传入函数的是类型是 Triangle 时,没有任何一个 case 符合,则不会有 return 语句执行,那么函数是默认返回 undefined。所以可以利用这个特点,结合 strictNullChecks编译选项,可以开启 strictNullChecks,然后让函数的返回值类型为 number,那么当返回 undefined 的时候,就会报错:
function getArea(s: Shape): number { // error Function lacks ending return statement and return type does not include 'undefined' switch (s.kind) { case "square": return s.size * s.size; case "rectangle": return s.height * s.width; case "circle": return Math.PI * s.radius ** 2; } } 复制代码
这种方法简单,但是对旧代码支持不好,因为strictNullChecks这个配置项是2.0版本才加入的,如果使用的是低于这个版本的,这个方法并不会有效。
(2)使用 never 类型
当函数返回一个错误或者不可能有返回值的时候,返回值类型为 never。所以可以给 switch 添加一个 default 流程,当前面的 case 都不符合的时候,会执行 default 后的逻辑:
function assertNever(value: never): never { throw new Error("Unexpected object: " + value); } function getArea(s: Shape) { switch (s.kind) { case "square": return s.size * s.size; case "rectangle": return s.height * s.width; case "circle": return Math.PI * s.radius ** 2; default: return assertNever(s); // error 类型“Triangle”的参数不能赋给类型“never”的参数 } } 复制代码
采用这种方式,需要定义一个额外的 asserNever 函数,但是这种方式不仅能够在编译阶段提示遗漏了判断条件,而且在运行时也会报错。
三、交叉类型
1. 交叉类型的使用
交叉类型是将多个类型合并为一个类型。 这让我们可以把现有的多种类型叠加到成为一种类型,合并后的类型将拥有所有成员类型的特性。交叉类型可以理解为多个类型的交集。
可以使用“&”操作符来声明交叉类型:
type Overlapping = string & number; 复制代码
如果我们仅仅把原始类型、字面量类型、函数类型等原子类型合并成交叉类型,是没有任何意义的,因为不会有变量同时满足这些类型,那这个类型实际上就等于never类型。
2. 交叉类型的使用场景
上面说了一般情况下使用交叉类型是没有意义的,那什么时候该使用交叉类型呢?下面就来看看交叉类型的使用场景。
(1)合并接口类型
将多个接口类型合并成为一个类型是交叉类型的一个常见的使用场景。这样就能相当于实现了接口的继承,也就是所谓的合并接口类型:
type Person = { name: string; age: number; } & { height: number; weight: number; } & { id: number; } const person: Person = { name: "zhangsan", age: 18, height: 180, weight: 60, id: 123456 } 复制代码
这里我们通过交叉类型使Person同时拥有了三个接口类型中的5个属性。
那如果两个接口中的同一个属性定义了不同的类型会发生了什么情况呢?
type Person = { name: string; age: number; } & { age: string; height: number; weight: number; } 复制代码
两个接口中都拥有age属性,并且类型分别是number和string,那么在合并后,age的类型就是string & number,就是一个 never 类型:
type Person = { name: string; age: number; } & { age: string; height: number; weight: number; } const person: Person = { name: "zhangsan", age: 18, // Type 'number' is not assignable to type 'never'.ts(2322) height: 180, weight: 60, } 复制代码
如果同名属性的类型兼容,比如一个是 number,另一个是 number 的子类型、数字字面量类型,合并后 age 属性的类型就是两者中的子类型:
type Person = { name: string; age: number; } & { age: 18; height: number; weight: number; } const person: Person = { name: "zhangsan", age: 20, // Type '20' is not assignable to type '18'.ts(2322) height: 180, weight: 60, } 复制代码
这里第二个接口中的age是一个数字字面量类型,它是number类型的子类型,所以合并之后的类型为字面量类型18。
(2)合并联合类型
交叉类型另外一个常见的使用场景就是合并联合类型。可以合并多个联合类型为一个交叉类型,这个交叉类型需要同时满足不同的联合类型限制,也就是提取了所有联合类型的相同类型成员:
type A = "blue" | "red" | 996; type B = 996 | 666; type C = A & B; const c: C = 996; 复制代码
如果多个联合类型中没有相同的类型成员,那么交叉出来的类型就是never类型:
type A = "blue" | "red"; type B = 996 | 666; type C = A & B; const c: C = 996; // Type 'number' is not assignable to type 'never