详解 TS 中的联合类型

简介

联合类型由一组有序的成员类型构成,联合类型表示类型可以为若干类型之一,等同于运算符 ||,类似于数学运算中的加法。需要注意的是联合类型是通过联合类型字面量来定义的。

联合类型字面量

联合类型由两个及以上的成员类型构成,成员类型通过竖线符号 "|" 分隔。

下面是一个联合类型字面量的例子:

type MyNumber = number | bigint

这里定义了一个 MyNumber 的联合类型,由 numberbigint 两个成员类型组成,表示 MyNumber 可以为 numberbigint 类型。

成员类型可以为任意类型

成员类型可以为任意类型,例如:布尔类型、字符串类型、字符串数组类型、函数类型、[[空对象类型字面量]]类型、对象类型,如下例所示:

type T = boolean | string | string[] | (() => void) | {} | { xnumber }

详解 TS 中的联合类型

成员类型合并/简化

  • 若成员类型有相同类型,则相同类型会被合并为一个(成员类型合并
  • 若成员类型之间有父子类型关系,则子类型会被消去(成员类型简化
type T0 = boolean;
type T1 = boolean | boolean;
type T2 = boolean | string | boolean;

这里 T1 由两个 boolean 类型构成,成员类型相同合并成一个 boolean 类型。

详解 TS 中的联合类型

这里 T2 由三个成员类型构成,其中两个相同的 boolean 被合并成一个。

详解 TS 中的联合类型

这里 T3 由 boolean string false 三个类型构成,其中 false 是[[布尔类型字面量]],它是 boolean 类型的子类型,最终被消去。

详解 TS 中的联合类型

满足“加法交换律”

成员类型是有序的但是与顺序无关满足加法交换律,交互成员类型先后顺序影响结果类型。

下例中联合类型都由顺序不同的相同成员类型构成,最终结果类型相同。

type T0 = string | number
type T1 = number | string

T0 类型的值

详解 TS 中的联合类型

T1 类型的值

详解 TS 中的联合类型

满足“加法结合律”

对成员类型满足加法结合律,使用分组运算符()不影响联合类型的结果类型。

下例中联合类型都由相同成员类型构成,对不同成员类型使用分组运算符,最终结果类型相同。

type T0 = (boolean | string) | number
type T1 = boolean | (string | number)

T0 类型的值

详解 TS 中的联合类型

T1 类型的值

详解 TS 中的联合类型

联合类型的类型成员

上面提到联合类型的成员类型可以为任意类型,当成员类型为基础类型时比较好理解,当成员类型为对象类型时联合类型的结果类型会怎样呢?

联合类型的类型成员由其成员类型决定,规则简要描述为:联合类型的类型成员由各个类型成员的属性成员的交集组成,属性成员的类型为各个成员类型的联合类型

对象类型分为对象类型字面量和接口,下面以对象类型字面量来详细描述交集的产生过程。

类型成员的交集

以这段代码为例

type A = {
 version: string;
 a: number;
}
type B = {
 version: string;
 b: string;
}
type T = A | B

其执行结果表明类型 A、类型 B 都可以赋值给类型 T,也就是类型 A、类型 B 都是类型 T 的子类型。

详解 TS 中的联合类型

简单标记为:T <- A T <- B ,同时根据[[鸭式辨型]]规则可以知道:

T <- A 相当于类型 A 上必须包含 T 上的属性成员 T <- B 相当于类型 B 上必须包含 T 上的属性成员

鸭式辨形:

类型 A 具有类型 B 的属性成员,则类型 A 是类型 B 的子类型,即类型 A 兼容类型 B,同时类型 A 可以赋值给类型 B,并且类型 A 上可以存在类型 B 上不存在的属性成员。

也就是说,一只动物游起来像鸭子,叫起来像鸭子,那么可以认为它就是一只鸭子。

所以,类型 T 上的属性成员只能是类型 A 和类型 B 属性成员的交集。

详解 TS 中的联合类型

这里代码执行结果证明类型 T 上确实只有 version 属性。

对象字面面类型或接口类型同时具有属性签名、索引签名、调用签名、构造签名,下面看看各签名在遇到联合类型时如何处理。

属性签名

相当于类型成员的交集中讲到的对象、接口的属性成员,所以联合类型的结果类型如上面分析的一样。

索引签名

索引签名的语法形式如下:

{
 [keynumber]: string;
}
接口
interface List {
 [keynumber]: any;
}

索引签名分为数字索引签名、字符串索引签名,如上代码所示,其作用是用来描述使用索引访问对象的属性的类型。像 obj.a obj[key] list[0] list[n] 这些都属于索引访问。

当联合类型的成员类型同时都具有数字索引签名或字符串索引签名时,结果类型中才包含相应的数字索引签名或字符串索引签名,并且结果类型的索引签名值类型是各成员类型索引签名值类型的联合类型,这也属于交集的概念。

这里 T0、T1 同时具有数字索引签名,因此结果类型 T0T1 具有数字索引签名,并且数字索引签名的类型是 string | number

详解 TS 中的联合类型

这里 T0、T1 分别具有数字索引签名和字符串索引签名,因此结果类型不具有索引签名,使用索引访问结果类型属性时会报错。

详解 TS 中的联合类型

调用签名

JavaScript 中函数也是对象,调用签名的作用正是用来描述对象是否可以被当做函数来调用以及能被当做函数调用时参数、返回值的类型,其语法形式如下(与[[函数签名]]类似):

{
 (idnumber): string;
}
或者
interface T {
 (idnumber): string;
}

当联合类型的成员类型同时都具有相同参数的调用签名时,结果类型中才包含调用签名,并且结果类型的调用签名值类型是各成员类型调用签名的联合类型,这也属于交集的概念。

这里 T0、T1 都具有调用签名,因此结果类型 T0T1 也具有调用签名。

详解 TS 中的联合类型

这里只有 T0 具有调用签名,因此结果类型 T0T1 不具有调用签名,对结果类型的值进行函数调用会报错。

详解 TS 中的联合类型

TODO: 当具有多个调用签名(函数重载)、相同的调用签名但参数类型不一样时,会如何?

构造签名

与调用签名类似,作用是描述对象是否可以作为构造函数被 new 运算符调用,其语法特性如下:

{
 new (id: number): string;
}
或者
interface T {
 new (id: number): string;
}

当联合类型的成员类型同时都具有相同参数的构造签名时,结果类型中才包含构造签名,并且结果类型的构造签名值类型是各成员类型构造签名的联合类型,这也属于交集的概念。

这里 T0、T1 都具有相同参数的构造函数,因此结果类型 T0T1 具有构造签名,且构造签名返回值类型为各各自的交集。

详解 TS 中的联合类型

这里仅 T0 具有构造签名,因此结果类型 T0T1 不具有构造签名,使用 new 运算符当做构造函数调用报错。

详解 TS 中的联合类型

属性修饰符

当联合类型成员类型为带有只读可选等属性修饰符的[[空对象类型字面量]]或接口时,只要其中某个成员类型具有属性修饰符,则联合类型的结果类型也具有相应的属性修饰符。

  • 只读 readonly

详解 TS 中的联合类型

  • 可选 ?

详解 TS 中的联合类型

一些疑难点

联合类型的“交集”如何理解?

type A = {
 version: string;
 a: number;
}
type B = {
 version: string;
 b: string;
}
type T = A | B

根据上面的“交集”思路分析,可知这里联合类型的结果类型 T 等价于 { version: string } ,但是既不是类型 A 也不是类型 B,这一点和联合类型表示成员类型之一相悖

解答如下:

上面结果类型 T 等价于 { version: string } 这个是 TeypeScript 类型系统内部机制。

从类型的兼容性角度来看,类型 A 可以赋值给 { version: string } ,类型 B 也可以赋值给 { version: string } ,也就是只要包含 version 属性的对象都可以赋值给 { version: string } ,所以可以说类型 T 是 A 类型或者 B 类型,这也是鸭式辩形的思想。

那是否只要包含 version 属性的对象都可以赋值给结果类型 T 呢?当然是不行的,联合类型在做类型检查时会对结果类型 { version: string } ,以及联合类型成员 A B 分别使用鸭式辩形来检查类型兼容性。当满足结果类型兼容性,但不满足成员类型兼容性时会报错,见下图。

详解 TS 中的联合类型

访问联合类型的属性报错怎么解释?

这段代码中定义的函数 fn 的参数为 numberstring 的联合类型,在函数体中访问形参的 length 属性会报错,使用上面联合类型的知识该如何解释呢?

function fn(data: number | string) {
    if (data.length > 0) { // Property 'length' does not exist on type 'string | number'.
           // Property 'length' does not exist on type 'number'.(2339)
    }
}

解答如下:

这里 number 为基础类型,会被自动封装为 Number 对象,所以number 类型声明可以查看 Number 接口。 Number 接口在 TypeScript 中的定义如下:

详解 TS 中的联合类型

可以看到,Number 接口上不存在 length 这一属性签名,根据上文联合类型“交集”思路很容易知道联合类型 number | string 的结果类型中不包含 length 属性签名,所以报错。