联合类型与交叉类型

TypeScript 中可以将多种类型连接为一种类型。连接方式共有两种。连接后形成的新类型分别为联合类型和交叉类型。

联合类型

联合类型使用 “|” 符号来连接多个类型。当一个变量为联合类型时,其取值可以为联合类型中的任何一个子类型。

例如,以下代码声明了一个类型别名 NumberOrString,它是联合类型 number |string,因此值既可以为字符串类型,又可以为数值类型。

type NumberOrString = number | string;
let a: NumberOrString;
a = "hello";
a = 1111;

联合类型中的子类型可以为任意类型,如类型别名、原始类型、引用类型、接口、类等。示例代码如下。

type A = NumberOrString | (() => void) | number[] | { x: string };
let a: A;
a = 1111;
a = "hello";
a = function () { };
a = [1, 2, 3];
a = { x: "hello" };

由于联合类型通常由多个类型组成,并不是一种具体的类型,就像 unknown 类型一样,因此在类型不明确时无法进行具体操作,否则将引起编译错误。示例代码如下。

function connection(a: string | boolean, b: string | boolean) {
    //编译错误:运算符"+"不能应用于类型"string | boolean"和"string | boolean"。ts(2365)
    return a + b;
}

只有先通过类型断言或类型防护(详见第 11 章)将联合类型的变量转换为某种已知的具体类型,才能进行具体操作。示例代码如下。

function connection(a: string | boolean, b: string | boolean) {
    if (typeof a == "boolean")
        return a && b;
    else
        return a + b;
}

如果联合类型中的子类型是对象类型、接口或类,那么为该联合类型的变量赋值时,至少需要完整满足其中一个对象类型、接口或类的结构。例如,以下代码定义了一个 Bird 对象类型和一个 Fish 对象类型,然后创建了一个名为 BirdOrFishOrBoth 的联合类型。在为该类型的变量赋值时,赋值的对象结构至少需要完整满足 Fish 对象类型或 Bird 对象类型中的一个。

type Bird = { name: string, wings: string, legs: number }
type Fish = { name: string, gills: string, fishScale: boolean }
type BirdOrFishOrBoth = Bird | Fish;

let a: BirdOrFishOrBoth;
//可以是Bird对象类型的结构
a = { name: "swallow", wings: "Small wings", legs: 2 };
//可以是Fish对象类型的结构
a = { name: "goldfish", gills: "small gill", fishScale: true };
//可以既是Bird对象类型的结构又是Fish对象类型的结构
a = { name: "a bird fish", gills: "big gill", wings: "big wings", legs: 2, fishScale:
true };
//可以完全满足Fish对象类型的结构,部分满足Bird对象类型的结构
a = { name: "a flying fish", gills: "big gill", fishScale: true, legs: 2 };
//可以完全满足Bird对象类型的结构,部分满足Fish对象类型的结构
a = { name: "a swiming bird", wings: "big wings", legs: 2, fishScale: false };

如果没有至少满足其中一个子类型的完整结构,就会引起编译错误。示例代码如下。

//编译错误:不能将类型"{ name: string; legs: number; fishScale: false; }"分配给类型
//"BirdOrFishOrBoth"。ts(2322)
a = { name: "", legs: 2, fishScale: false };

交叉类型

交叉类型使用 & 符号来连接多个类型。当一个变量为交叉类型时,该变量的取值类型必须同时符合交叉类型中的所有子类型。

交叉类型无法用于原始类型,因为会出现交叉后的类型是 never 类型的情况。例如,以下代码声明了一个名为 number & string 的交叉类型,但由于没有任何值同时属于数值和字符串类型,因此编译器会将该类型识别为 never 类型,表示它不可能存在值。

type NumberAndString = number & string;

//编译错误:不能将类型"number"分配给类型"never"。ts(2322)
let a:NumberAndString = 1;

交叉类型通常只用于对象类型、接口或类的连接,产生的交叉类型将拥有各个子类型的全部成员。例如,在以下代码中,先分别声明了一个 Colorful 接口和一个 Circle 接口,它们拥有各自的属性和方法,然后声明了一个名为 ColorfulCircle 的类型别名,该类型别名的具体类型是 ColorfulCircle 接口的交叉类型,因此 ColorfulCircle 将具有两个接口的所有属性和方法,最后声明了一个 ColorfulCircle 类型的变量 circle1,并为该变量赋予了一个符合 ColorfulCircle 类型结构的对象。

interface Colorful {
    color: string
}

interface Circle {
    radius: number,
    rollling: () => void
}

type ColorfulCircle = Colorful & Circle;
let circle1: ColorfulCircle = {
    color: "red",
    radius: 5,
    rollling: function () { console.log("圆环滚动中!") }
}

和联合类型不同,当为交叉类型的变量赋值时,这个值必须完全满足交叉类型中全部子类型的所有结构;否则,将引起编译错误。示例代码如下。

//编译错误:不能将类型"{ color: string; }"分配给类型"ColorfulCircle"。缺少以下属
//性: radius, rollling。ts(2322)
circle1 = { color: "blue" }

//编译错误:不能将类型"{ radius: number; rollling: () => void; }"分配给类型
//"ColorfulCircle"。缺少属性 "color"。ts(2322)
circle1 = {
    radius: 5,
    rollling: function () { console.log("圆环滚动中!") }
}

从理论上来说,可以同时使用交叉类型和联合类型创建新类型。示例代码如下。但实际编程中切勿使用这种方式,因为这种方式会大大降低代码的可读性和可维护性,并且极易导致新问题。

//以下代码等同于{x:number,y:number} | { z: number };
type A = { x: number } & { y: number } | { z: number };