详解 TS 中的联合类型
简介
联合类型由一组有序的成员类型构成,联合类型表示类型可以为若干类型之一,等同于运算符 ||
,类似于数学运算中的加法。需要注意的是联合类型是通过联合类型字面量来定义的。
联合类型字面量
联合类型由两个及以上的成员类型构成,成员类型通过竖线符号 "|"
分隔。
下面是一个联合类型字面量的例子:
type MyNumber = number | bigint
这里定义了一个 MyNumber
的联合类型,由 number
和 bigint
两个成员类型组成,表示 MyNumber
可以为 number
或 bigint
类型。
成员类型可以为任意类型
成员类型可以为任意类型,例如:布尔类型、字符串类型、字符串数组类型、函数类型、[[空对象类型字面量]]类型、对象类型,如下例所示:
type T = boolean | string | string[] | (() => void) | {} | { x: number }
成员类型合并/简化
- 若成员类型有相同类型,则相同类型会被合并为一个(成员类型合并)
- 若成员类型之间有父子类型关系,则子类型会被消去(成员类型简化)
type T0 = boolean;
type T1 = boolean | boolean;
type T2 = boolean | string | boolean;
这里 T1 由两个 boolean
类型构成,成员类型相同合并成一个 boolean
类型。
这里 T2 由三个成员类型构成,其中两个相同的 boolean
被合并成一个。
这里 T3 由 boolean
string
false
三个类型构成,其中 false
是[[布尔类型字面量]],它是 boolean
类型的子类型,最终被消去。
满足“加法交换律”
成员类型是有序的但是与顺序无关满足加法交换律,交互成员类型先后顺序影响结果类型。
下例中联合类型都由顺序不同的相同成员类型构成,最终结果类型相同。
type T0 = string | number
type T1 = number | string
T0 类型的值
T1 类型的值
满足“加法结合律”
对成员类型满足加法结合律,使用分组运算符()
不影响联合类型的结果类型。
下例中联合类型都由相同成员类型构成,对不同成员类型使用分组运算符,最终结果类型相同。
type T0 = (boolean | string) | number
type T1 = boolean | (string | number)
T0 类型的值
T1 类型的值
联合类型的类型成员
上面提到联合类型的成员类型可以为任意类型,当成员类型为基础类型时比较好理解,当成员类型为对象类型时联合类型的结果类型会怎样呢?
联合类型的类型成员由其成员类型决定,规则简要描述为:联合类型的类型成员由各个类型成员的属性成员的交集组成,属性成员的类型为各个成员类型的联合类型。
对象类型分为对象类型字面量和接口,下面以对象类型字面量来详细描述交集的产生过程。
类型成员的交集
以这段代码为例
type A = {
version: string;
a: number;
}
type B = {
version: string;
b: string;
}
type T = A | B
其执行结果表明类型 A、类型 B 都可以赋值给类型 T,也就是类型 A、类型 B 都是类型 T 的子类型。
简单标记为: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 属性成员的交集。
这里代码执行结果证明类型 T 上确实只有 version
属性。
对象字面面类型或接口类型同时具有属性签名、索引签名、调用签名、构造签名,下面看看各签名在遇到联合类型时如何处理。
属性签名
相当于类型成员的交集中讲到的对象、接口的属性成员,所以联合类型的结果类型如上面分析的一样。
索引签名
索引签名的语法形式如下:
{
[key: number]: string;
}
接口
interface List {
[key: number]: any;
}
索引签名分为数字索引签名、字符串索引签名,如上代码所示,其作用是用来描述使用索引访问对象的属性的类型。像 obj.a
obj[key]
list[0]
list[n]
这些都属于索引访问。
当联合类型的成员类型同时都具有数字索引签名或字符串索引签名时,结果类型中才包含相应的数字索引签名或字符串索引签名,并且结果类型的索引签名值类型是各成员类型索引签名值类型的联合类型,这也属于交集的概念。
这里 T0、T1 同时具有数字索引签名,因此结果类型 T0T1 具有数字索引签名,并且数字索引签名的类型是 string | number
。
这里 T0、T1 分别具有数字索引签名和字符串索引签名,因此结果类型不具有索引签名,使用索引访问结果类型属性时会报错。
调用签名
JavaScript 中函数也是对象,调用签名的作用正是用来描述对象是否可以被当做函数来调用以及能被当做函数调用时参数、返回值的类型,其语法形式如下(与[[函数签名]]类似):
{
(id: number): string;
}
或者
interface T {
(id: number): string;
}
当联合类型的成员类型同时都具有相同参数的调用签名时,结果类型中才包含调用签名,并且结果类型的调用签名值类型是各成员类型调用签名的联合类型,这也属于交集的概念。
这里 T0、T1 都具有调用签名,因此结果类型 T0T1 也具有调用签名。
这里只有 T0 具有调用签名,因此结果类型 T0T1 不具有调用签名,对结果类型的值进行函数调用会报错。
TODO: 当具有多个调用签名(函数重载)、相同的调用签名但参数类型不一样时,会如何?
构造签名
与调用签名类似,作用是描述对象是否可以作为构造函数被 new
运算符调用,其语法特性如下:
{
new (id: number): string;
}
或者
interface T {
new (id: number): string;
}
当联合类型的成员类型同时都具有相同参数的构造签名时,结果类型中才包含构造签名,并且结果类型的构造签名值类型是各成员类型构造签名的联合类型,这也属于交集的概念。
这里 T0、T1 都具有相同参数的构造函数,因此结果类型 T0T1 具有构造签名,且构造签名返回值类型为各各自的交集。
这里仅 T0 具有构造签名,因此结果类型 T0T1 不具有构造签名,使用 new
运算符当做构造函数调用报错。
属性修饰符
当联合类型成员类型为带有只读、可选等属性修饰符的[[空对象类型字面量]]或接口时,只要其中某个成员类型具有属性修饰符,则联合类型的结果类型也具有相应的属性修饰符。
- 只读
readonly
- 可选
?
一些疑难点
联合类型的“交集”如何理解?
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
分别使用鸭式辩形来检查类型兼容性。当满足结果类型兼容性,但不满足成员类型兼容性时会报错,见下图。
访问联合类型的属性报错怎么解释?
这段代码中定义的函数 fn 的参数为 number
与 string
的联合类型,在函数体中访问形参的 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 中的定义如下:
可以看到,Number
接口上不存在 length
这一属性签名,根据上文联合类型“交集”思路很容易知道联合类型 number | string
的结果类型中不包含 length
属性签名,所以报错。