联合类型用 | 列出几个候选类型:类型为 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 的类型守卫函数。