Menu

TypeScript 交叉类型:用 & 组合多个类型

交叉类型 A & B 描述的是同时属于 A 和 B 的值,所以它拥有两边的全部成员。本文讲解如何用 & 组合对象类型、冲突的属性为什么会变成 never、联合类型取交叉后为什么只保留共同的成员,以及什么时候应该改用 extends。

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

交叉类型写作 A & B,描述的是同时是 A 又是 B 的值。对于对象类型,这意味着值拥有两边的每一个属性。用它可以组合已有的类型,而不必把属性重新写一遍。

第二个对象缺少 team,所以会出现编译错误 TS2322,错误信息的下一行写着 Property 'team' is missing ... but required in type 'Employee'。凡是需要 Person 或 Employee 的地方,都可以传入 Staff 值。

组合对象类型

& 可以任意组合类型别名、接口和内联对象类型,也可以用于泛型类型参数。最后这种情况是它难以替代的地方:一个给传入对象添加属性的函数,可以精确地表达这一点。

调用者既保留了传入对象的确切类型(title、words),又得到了新增的两个属性。interface ... extends 无法表达“不管 T 是什么,再加上这些属性”,因为接口不能继承类型参数。

冲突的属性会变成 never

当两边声明了同一个属性时,这个属性的类型是两者的交叉。如果这两个类型没有共同的值,属性就会变成 never,而编译器在你真正创建值之前一声不吭:

index.ts(7,22): error TS2322: Type 'string' is not assignable to type 'never'.

错误指向的是对象,而不是导致问题的类型,所以这类 bug 很难追查。在编辑器里把鼠标悬停在 r.id 上,可以看到它的类型:never。如果冲突的属性是字面量标签,例如 type Shape = { kind: "circle" } & { kind: "square" },TypeScript 会更进一步,把整个交叉类型化简为 never。此时读取这种值的属性,错误信息会说明原因:Property 'kind' does not exist on type 'never'. The intersection 'Shape' was reduced to 'never' because property 'kind' has conflicting types in some constituents.

interface ... extends 则会在声明处就发现同样的冲突,报错 TS2430。这是两者在实践中的主要区别;interface 与 type 的对比页面把它们放在一起做了比较。

兼容的重叠会收窄属性

如果两个属性类型确实有重叠,结果就是重叠的部分。这很有用,并不是错误:

后半部分展示了 & 对联合类型的作用:它保留两边共有的成员。把类型看作值的集合,这些结果就很好预测了。A | B 是两个集合的并集,A & B 是它们的交集,而空的交集就是 never。

交叉类型与联合类型

这两个名字来自集合论,用在对象属性上时会让人觉得是反过来的:

A | B(联合类型)A & B(交叉类型)
值是A 或 B既是 A 又是 B
允许的值的集合更大更小
可以使用的属性只有两边都有的任意一边的全部属性
string 与 numberstring | numbernever
"a" | "b" 与 "b" | "c""a" | "b" | "c""b"

对象类型的交叉类型之所以拥有更多属性,正是因为它允许的值更少:只有具备全部属性的对象才行。

交叉类型与 extends

type C = A & Binterface C extends A, B
适用于任何类型,包括联合类型和类型参数成员静态已知的对象类型
冲突的属性悄悄变成 never在声明处报错 TS2430 或 TS2320
结果一个交叉类型,按组成部分逐个检查一个扁平的具名类型,类型关系会被缓存
大型组合可能拖慢类型检查TypeScript wiki 的 Performance 页面推荐使用

组合几个对象类型别名时,& 是惯用写法,没有问题。对于由很多部分构成的类型,或者公开 API 的类型,extends 能更早报错,类型检查的开销也更小。

与基本类型交叉:品牌类型

把基本类型和对象类型交叉不会得到 never:string & { readonly __brand: "UserId" } 是一个带有额外标记的字符串,这个标记只在编译时存在。没有哪个真实的字符串拥有这个属性,而这恰恰是目的所在:只有刻意断言品牌的代码才能创建它,所以普通的 string 或 OrderId 再也不能传给需要 UserId 的地方。这种技巧有专门的页面介绍:品牌类型。

常见问题

TypeScript 中的交叉类型是什么?

写作 A & B 的类型,它的值必须同时满足 A 和 B。对于对象类型,这意味着值拥有 A 的每个属性,也拥有 B 的每个属性。这是把两个类型别名合并成一个的常用方法。

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

联合类型 A | B 表示“二者之一”:值可以是 A 也可以是 B,在收窄之前你只能使用两者共有的成员。交叉类型 A & B 表示“两者都是”:值拥有两边的全部内容。对于对象类型,联合类型接受的值更多,交叉类型拥有的属性更多。

为什么我的交叉类型变成了 never?

因为没有任何值能同时满足两边。string & number 是 never,而 { id: string } & { id: number } 让 id 变成 string & number,于是这个属性是 never,任何对象都创建不出来。如果两个对象类型有同名的字面量标签但值不同(kind: "circle" 和 kind: "square"),整个交叉类型会化简为 never。

应该用交叉类型还是 extends?

两者都能组合对象类型。interface X extends A, B 会在声明处报告冲突的属性,TypeScript 团队也推荐用它来组合大型对象类型。& 适用于任何类型,包括联合类型和泛型参数,这些是 extends 无法组合的。类型别名和泛型工具函数用 &,构建接口时用 extends。

TypeScript 中如何合并两个对象类型?

写 type Merged = A & B。运行时的值则把两个对象展开:const merged: A & B = { ...a, ...b }。如果 A 和 B 有同名但类型不同的属性,这个属性的类型会变成 never;如果第二个对象的属性应当覆盖第一个对象的,就用 Omit<A, keyof B> & B。

Coddy 编程语言插图

和 Coddy 一起学编程

开始