Menu

TypeScript 联合类型:| 运算符用法与示例

像 string | number 这样的联合类型表示一个值可以是几种类型中的任意一种。本文讲解联合类型能做什么(只能用所有成员都支持的操作)、如何收窄、字面量联合与对象联合,以及 (A | B)[] 和 A[] | B[] 的区别。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

联合类型用 | 列出几个候选类型:类型为 string | number 的值要么是字符串,要么是数字。TypeScript 用联合类型描述那些本来就可能有多种形态的值,并且在你使用某种形态特有的功能之前,编译器会要求你先检查手上是哪一种。

在 typeof 检查的每个分支里,id 都只有一种类型。这一步叫作收窄(narrowing),正是它让联合类型变得实用。

只能使用共有成员

收窄之前,你只能使用联合类型中每个成员都支持的操作。toString() 在字符串和数字上都存在,所以没问题;toUpperCase() 只有字符串才有:

index.ts(3,16): error TS2339: Property 'toUpperCase' does not exist on type 'string | number'.
  Property 'toUpperCase' does not exist on type 'number'.

第二行指出了缺少该属性的是哪个成员。反过来也是同样的规则:string | number 类型的值不能传给类型为 string 的参数(TS2345),因为它可能是数字。联合类型能接受更多的值,代价是在你检查之前能对它做的事更少。

收窄联合类型

收窄用的是普通的 JavaScript 检查。TypeScript 会跟踪控制流,把已经排除的成员一个个去掉,所以到最后一个检查之后只剩下一个成员:

检查收窄到适用于
typeof x === "string"对应的基本类型string、number、boolean、bigint、symbol、undefined、function
x === null、x === "a"比较的那个值null、undefined、字面量成员
Array.isArray(x)数组成员数组
x instanceof Date对应的类类的实例
"meow" in x拥有该属性的成员对象类型
x.kind === "circle"带有该标签的成员可辨识联合
isCat(x)(返回 x is Cat)函数声明的类型任意类型,自定义逻辑

完整的收窄方式请看类型收窄一页。

字面量类型的联合

由字面量值组成的联合类型就是一组封闭的可选值。这是实际代码中最常见的联合类型:

type Status = "idle" | "loading" | "success" | "error";
type Dice = 1 | 2 | 3 | 4 | 5 | 6;
type Toggle = "on" | "off" | boolean; // boolean is itself true | false

let current: Status = "idle";
current = "loading";   // fine
current = "finished";  // error TS2322: Type '"finished"' is not assignable to type 'Status'.

与字面量比较也会收窄:在 if (current === "error") 之后,else 分支知道 current 是另外三个值之一。在很多代码库中,字面量联合取代了枚举;关于 as const 以及如何从数组推导出这样的联合类型,请参阅字面量类型。

对象类型的联合

当成员是对象类型时,所有成员共有的属性可以直接访问。其余属性要先用 in 检查是否存在:

如果联合类型包含好几种对象形状,更清晰的写法是加一个共有的字面量属性,例如 kind: "cat" / kind: "fish"。只需检查这一个属性就能收窄整个对象,对它使用 switch 还能检查是否穷尽了所有情况。这种模式就是可辨识联合。

数组与联合类型

括号的位置会彻底改变含义:

类型含义示例值
(string | number)[]一个数组,每个元素是字符串或数字[1, "two", 3]
string[] | number[]一个全是字符串的数组,或一个全是数字的数组["a", "b"]
string | number[]一个字符串,或一个数字数组(| 的优先级低于 [])"text"

遍历 (string | number)[] 时,每个元素都是联合类型,需要先收窄,就像上面 reduce 回调里那样。map 和 filter 这类方法也可以用在 string[] | number[] 上,回调收到的参数类型是 string | number。

与 null 和 undefined 的联合

最常见的联合类型是“一个值或者什么都没有”:string | null、User | undefined。Array.prototype.find 和 Map.prototype.get 返回的就是这种类型,可选属性 name?: string 读取时的类型也是 string | undefined。用 ?.、?? 和 null 检查来处理它们的内容在单独的一页:null 和 undefined。

要在类型层面从已有的联合类型中去掉某些成员,可以使用内置工具类型:Exclude<"a" | "b" | "c", "a"> 得到 "b" | "c",NonNullable<string | null> 得到 string。

常见问题

TypeScript 中的联合类型是什么?

由多个候选类型用 | 连接而成的类型。类型为 string | number 的值可以是字符串,也可以是数字。在你用 typeof value === "string" 这样的检查把值收窄到某一个成员之前,编译器只允许你使用所有成员共有的部分。

为什么 TypeScript 提示联合类型上不存在某个属性?

因为联合类型中至少有一个成员没有这个属性。错误 TS2339,例如 Property 'toUpperCase' does not exist on type 'string | number',意思是这个值可能是数字,而数字没有 toUpperCase。先收窄(typeof、in、Array.isArray、instanceof 或判别属性检查),再使用该成员特有的属性。

如何声明一个能存放多种类型的数组?

把联合类型放进括号:(string | number)[] 或 Array<string | number>,每个元素都可以是其中任一类型。string[] | number[] 则不同:整个数组要么全是字符串,要么全是数字。不加括号时,string | number[] 表示一个字符串或一个数字数组。

联合类型和交叉类型有什么区别?

联合类型 A | B 的值是其中一种类型,所以你只能使用它们共有的部分。交叉类型 A & B 的值同时是两种类型,所以拥有两者的全部成员。对于对象类型,A | B 接受的值更多,A & B 要求的属性更多。

如何判断联合类型的值属于哪种类型?

使用 TypeScript 能理解的运行时检查:基本类型用 typeof x === "string",数组用 Array.isArray(x),类用 x instanceof Date,对象形状用 "prop" in x,成员带有相同的字面量标签时用 x.kind === "circle"。需要自定义逻辑时,编写一个返回 x is T 的类型守卫函数。

Coddy 编程语言插图

和 Coddy 一起学编程

开始