交叉类型

交叉类型在逻辑上与联合类型是互补的。联合类型表示一个值的类型为多种类型之一,而交叉类型则表示一个值同时属于多种类型。

交叉类型通过交叉类型字面量来定义。

交叉类型字面量

交叉类型由两个或多个成员类型构成,各成员类型之间使用 & 符号分隔。示例如下:

interface Clickable {
    click(): void;
}
interface Focusable {
    focus(): void;
}

type T = Clickable & Focusable;

此例中,定义了一个名为 T 的交叉类型。该交叉类型由两个成员类型组成,即 Clickable 类型和 Focusable 类型。若一个值既是 Clickable 类型又是 Focusable 类型,那么我们说该值的类型为交叉类型 Clickable & Focusable。它表示既可以点击又可以获得焦点的对象。

成员类型的运算

与联合类型相似,如果交叉类型中存在多个相同的成员类型,那么相同的成员类型将被合并为单一成员类型。示例如下:

type T0 = boolean;
type T1 = boolean & boolean;
type T2 = boolean & boolean & boolean;

此例中,T0T1T2 都表示同一种类型 boolean

交叉类型是有序的成员类型的集合。在绝大部分情况下,成员类型满足类似于数学中的 加法交换律,即改变成员类型的顺序不影响交叉类型的结果类型。例如,下例中虽然交叉类型 T0T1 中成员类型的定义顺序不同,但是 T0T1 表示相同的类型:

interface Clickable {
    click(): void;
}
interface Focusable {
    focus(): void;
}

type T0 = Clickable & Focusable;
type T1 = Focusable & Clickable;

需要注意的是,当交叉类型涉及调用签名重载或构造签名重载时便失去了 加法交换律 的性质。因为交叉类型中成员类型的顺序将决定重载签名的顺序,进而将影响重载签名的解析顺序。示例如下:

interface Clickable {
    register(x: any): void;
}
interface Focusable {
    register(x: string): boolean;
}

type ClickableAndFocusable = Clickable & Focusable;
type FocusableAndFocusable = Focusable & Clickable;

function foo(
    clickFocus: ClickableAndFocusable,
    focusClick: FocusableAndFocusable
) {
    let a: void = clickFocus.register('foo');
    let b: boolean = focusClick.register('foo');
}

此例第 8 行和第 9 行使用不同的成员类型顺序定义了两个交叉类型。第 15 行,调用 register() 方法的返回值类型为 void,说明在 ClickableAndFocusable 类型中,Clickable 接口中定义的 register() 方法具有更高的优先级。第 16 行,调用 register() 方法的返回值类型为 boolean,说明 FocusableAndFocusable 类型中 Focusable 接口中定义的 register() 方法具有更高的优先级。此例也说明了调用签名重载的顺序与交叉类型中成员类型的定义顺序是一致的。

交叉类型中的类型成员同样满足类似于数学中的 加法结合律。对部分类型成员使用分组运算符不影响交叉类型的结果类型。例如,下例中的 T0T1 类型是同一种类型:

interface Clickable {
  click(): void;
}
interface Focusable {
  focus(): void;
}
interface Scrollable {
  scroll(): void;
}

type T0 = (Clickable & Focusable) & Scrollable;
type T1 = Clickable & (Focusable & Scrollable);

原始类型

交叉类型通常与对象类型一起使用。虽然在交叉类型中也允许使用原始类型成员,但结果类型将成为 never 类型,因此在实际代码中并不常见。示例如下:

type T = boolean & number & string;

此例中,类型 Tbooleannumberstring 类型组成的交叉类型。根据交叉类型的定义,若一个值是 T 类型,那么该值既是 boolean 类型,又是 number 类型,还是 string 类型。显然,不存在这样一个值,所以 T 类型为 never 类型。never 类型是尾端类型,是一种不存在可能值的类型。

关于尾端类型的详细介绍请参考 5.8 节。

交叉类型的类型成员

属性签名

只要交叉类型 I 中任意一个成员类型包含了属性签名 M,那么交叉类型I也包含属性签名 M。例如,有以下的接口类型 AB

interface A {
    a: boolean;
}

interface B {
    b: string;
}

那么,接口类型 AB 的交叉类型 A & B 为如下对象类型:

{
    a: boolean;
    b: string;
}

对于交叉类型的属性签名,其类型为所有成员类型中该属性类型的交叉类型。例如,有以下接口类型 AB

interface A {
    x: { a: boolean };
}
interface B {
    x: { b: boolean };
}

那么,接口类型 AB 的交叉类型 A & B 为如下对象类型:

{
    x: { a: boolean } & { b: boolean }
}

该类型也等同于如下类型:

{
    x: {
        a: boolean;
        b: boolean;
    };
}

若交叉类型的属性签名 M 在所有成员类型中都是可选属性,那么该属性签名在交叉类型中也是可选属性。否则,属性签名 M 是一个必选属性。例如,有以下接口类型 AB

interface A {
    x: boolean;
    y?: string;
}
interface B {
    x?: boolean;
    y?: string;
}

那么,接口类型 AB 的交叉类型 A & B 为如下对象类型:

{
    x: boolean;
    y?: string;
}

A & B 交叉类型中,属性 x 是必选属性,属性 y 是可选属性。

索引签名

如果交叉类型中任何一个成员类型包含了索引签名,那么该交叉类型也拥有了索引签名;否则,该交叉类型没有索引签名。例如,有以下接口类型 AB

interface A {
    [prop: string]: string;
}
interface B {
    [prop: number]: string;
}

那么,接口类型 AB 的交叉类型 A & B 为如下对象类型,它同时包含了字符串索引签名和数值索引签名:

{
    [prop: string]: string;
    [prop: number]: string;
}

交叉类型索引签名中的索引值类型为每个成员类型中索引值类型的交叉类型。例如,有以下接口类型 AB

interface A {
    [prop: string]: { a: boolean };
}
interface B {
    [prop: string]: { b: boolean };
}

那么,接口类型 AB 的交叉类型 A & B 为如下对象类型:

{
    [prop: string]: { a: boolean } & { b: boolean };
}

该类型也等同于如下类型:

{
    [prop: string]: {
        a: boolean;
        b: boolean;
    };
}

调用签名与构造签名

若交叉类型的成员类型中含有调用签名或构造签名,那么这些调用签名和构造签名将以成员类型的先后顺序合并到交叉类型中。例如,有以下接口类型 AB

interface A {
    (x: number): number;
}
interface B {
    (x: string): string;
}

那么交叉类型 A & B 为如下对象类型:

{
    (x: boolean): boolean;
    (x: string): string;
}

同时,交叉类型 B & A 为如下对象类型:

{
    (x: string): string;
    (x: boolean): boolean;
}

通过这两个例子能够看到,交叉类型中调用签名的顺序与交叉类型类型成员的顺序相同,构造签名同理。当交叉类型中存在重载签名时,需要特别留意类型成员的定义顺序。

交叉类型与联合类型

优先级

当表示交叉类型的 & 符号与表示联合类型的 | 符号同时使用时,& 符号具有更高的优先级。& 符号如同数学中的乘法符号 ×,而 | 符号则如同数学中的加法符号 +。例如,有如下复合类型:

A & B | C & D

该类型等同于如下类型:

(A & B) | (C & D)

还要注意,当表示交叉类型的 & 符号与表示联合类型的 | 符号与函数类型字面量同时使用时,& 符号和 | 符号拥有更高的优先级。例如,有如下类型:

() => bigint | number

该类型等同于如下类型,即返回值类型为联合类型 bigint | number 的函数类型:

() => (bigint | number)

而不是函数类型和 number 类型的联合类型,如下所示:

(() => bigint) | number

在任何时候,我们都可以使用分组运算符 () 来明确指定优先级。

分配律性质

由交叉类型和联合类型组成的类型满足类似于数学中乘法分配律的规则。表示交叉类型的 & 符号如同数学中的乘法符号 ×,而表示联合类型的 | 符号则如同数学中的加法符号 +。下例中的 ≡ 符号是恒等号,表示符号两侧是恒等关系:

A & (B | C) ≡ (A & B) | (A & C)

另一个稍微复杂的示例如下所示:

(A | B) & (C | D) ≡ A & C | A & D | B & C | B & D

如果我们对照着数学中的 乘法分配律,那么此例中的结果将不难理解。

了解了交叉类型与联合类型的分配律性质后,我们就能够分析与理解一些复杂的类型。例如,有如下的复合类型:

T = (string | 0) & (number | 'a');

利用上文介绍的规则将该类型展开就能得到最终的结果类型,如下所示:

T = (string | 0) & (number | 'a');

  = (string & number) | (string & 'a') | (0 & number) | (0 & 'a');

  // 没有交集的原始类型的交叉类型是 'never' 类型
  = never | 'a' | 0 | never;

  // 'never' 尾端类型是所有类型的子类型
  // 并且若某成员是其他成员的子类型,则可以从联合类型中消去
  = 'a' | 0;