likes
comments
collection
share

TypeScript 的 & 与 |

作者站长头像
站长
· 阅读数 15

最近在工作中使用 TypeScript 开发 Web 系统的前后端,然后发现自己对 TypeScript 的 &(Intersection Types,交叉类型)与 |(Union Types,联合类型)理解并不深刻,它们的行为与我的预期不符。在经历了一些探索之后才有大概的了解,在此总结一下。

实验结果

对于 TypeScript 基础类型而言,交叉类型看起来像是类型的交集,联合类型则像是类型的并集。

type Intersection = string & number
// Intersection = never

type Union = string | number
// Union = string | number

对于 TypeScript 对象类型而言,交叉类型是对象属性的并集,联合类型则是对象属性的交集。同时,可以注意到,两个对象间存在同一属性不同类型时,对这一属性的类型做了当前运算运算符表示的运算(注意观察属性 c 的两个不同结果)。

type A = {
  a: number;
  b: number;
  c: number;
};

type B = {
  b: number;
  c: string;
  d: number;
};

type C = {
  x: number;
};

type D = A & B;
/*
D = {
  a: number;
  b: number;
  c: never; // Equal to string & number
  d: number;
}
*/

type E = A | B;
/*
E = {
  b: number;
  c: string | number;
}
*/

type F = A & C;
/*
F = {
  a: number;
  b: number;
  c: number;
  x: number;
}
*/

type G = A | C;
/*
G = {}
*/

一个重要观点

对象类型要求它所描述的对象 这些属性,而不是 有且仅有 这些属性。

type A = {
  x: number;
};

type B = {
  x: number;
  y: number;
};

type flag = B extends A ? true : false;
// flag = true, this shows that B is a subtype of A

const obj = {
  x: 1,
  x1: 1,
};

const a: A = obj;
const b: B = obj; // ERROR: Property 'y' is missing in type '{ x: number; x1: number; }' but required in type 'B'.

可推导出的重要结论

  • 对象类型描述了若干对象的集合,而非其属性的集合
  • 当且仅当一个对象具有对象类型所描述的全部属性,该对象属于该类型的成员
  • 类型 B 之所以是类型 A 的子类型,是因为类型 A 是所有具有 x 属性的对象的集合,而类型 B 也具有 x 属性,同时,类型 B 还要求具有 y 属性,比类型 A 的要求更严格
  • 交叉类型 A & B 具有类型 AB 的所有属性(限定最严格),联合类型 A | B 具有类型 AB 的共有属性(限定最宽松)

运算优先级

&| 有更高的运算优先级。

运算律

实验结果在此略去。

对于交集运算符 &

  • 唯一性:A & A 等价于 A
  • 交换律:A & B 等价于 B & A
  • 结合律:(A & B) & C 等价于 A & (B & C)
  • 父类型收敛:当且仅当 BA 的父类型时,A & B 等价于 A

对于并集运算符 |

  • 唯一性:A | A 等价于 A
  • 交换律:A | B 等价于 B | A
  • 结合律:(A | B) | C 等价于 A | (B | C)
  • 子类型收敛:当且仅当 BA 的子类型时,A | B 等价于 A

& 与函数重载

如果 AB 是函数类型,那么 A & B 代表重载函数,此时,集合运算顺序即为重载函数的函数签名顺序。

type M = (a: number) => number;
type N = (a: string) => string;

function overload(a: number): number;
function overload(a: string): string;
function overload(a: number | string) {
  return a;
}

const func: M & N = overload;

func()
/*
IntelliSense:
[1/2] func(a: number): number
[2/2] func(a: string): string
*/

& 与类型别名区分(make a type alias nominal)

类型别名只是别名——它们与它们引用的类型没有区别,但有些时候需要在类型上区分开两个同样基本数据类型的数据,这时就需要使用 & 来为类型别名打上一个类似 Tag 的东西。

// Strings here are arbitrary, but must be distinct
type SomeUrl = string & { 'this is a url': {} };
type FirstName = string & { 'person name': {} };

// Add type assertions
let x = <SomeUrl>'';
let y = <FirstName>'bob';
x = y; // ERROR: Type 'FirstName' is not assignable to type 'SomeUrl'. Property ''this is a url'' is missing in type 'String & { 'person name': {}; }' but required in type '{ 'this is a url': {}; }'.

参考资料

  1. 【Typescript】进击的基础(一)交叉类型和联合类型-集合论角度理解
  2. Intersection types #1256
  3. Spec Preview: Union types #805
  4. TypeScript FAQs - Can I make a type alias nominal?