TypeScript 联合、交叉类型 & 代数类型

发布于

首先要说明的是 TypeScript 是一个 structural type system,与之相对应的是 nominal type system(采用的语言有 C++/Java/C#/Rust 等)。二者的具体区别可以查看维基百科。

二者的确十分相似,文档上说:

Type aliases and interfaces are very similar, and in many cases you can choose between them freely. Almost all features of an interface are available in type, the key distinction is that a type cannot be re-opened to add new properties vs an interface which is always extendable.

几乎 interface 上所有的功能,type 上都有,主要区别在于 type 不能 re-open 添加新的属性,而 interface 总是可扩展的。 文档上比较有代表性的点:

建议查看官方文档上的 playgound 例子。

product type / sum type, 参考了这个回答

enum Animal {
  Human = 0,
  Land = 1,
  Water = 2,
  Sky = 3,
}

type A = bool | Animal; // 和类型,它的值有 2 + 4 = 6 种可能

interface B {
  creature: Animal;
  alive: bool;
} // 积类型,它的值有 2 * 4 = 8 种类型

在 TypeSript 中存在 3 个特殊的类型,any unknown never,他们的区别如下:

关于 never 能赋值给任何类型:

declare const carNo: unique symbol;

interface Car {
  [carNo]: void;
}

type fn = () => Car;

// getCar 实际上返回的就是 never 但是能和 fn 兼容
const getCar = () => {
  throw new Error();
};

const myCar: Car = getCar();

intersection (交叉类型) vs union (联合类型)

标题部分 intersection (交叉类型) vs union (联合类型)

intersection 得到的是 子类型union 得到的是 父类型

以下面这个 type-util 为例。 首先需要知道的是:如果一个函数是另一个函数的子类型,那么这个函数的参数的类型就是另一个函数的父类型。所以这里 U 是 I 的父类型,而且 infer 出来的是一个具体的类型,所以就是 intersection 了。

export type UnionToIntersection<U> = (
  U extends any ? (k: U) => void : never
) extends (k: infer I) => void
  ? I
  : never;

下面举例说明一下 A | BA & B 的区别。

示例 1:当 A 和 B 的属性的类型「不存在」矛盾时

标题部分 示例 1:当 A 和 B 的属性的类型「不存在」矛盾时

playground link

interface Dog {
  age: number;
  bark: Function;
}

interface Cat {
  age: number;
  meow: Function;
}

// intersection types
type Animal1 = Dog & Cat;

const unknownAnimal1: Animal1 = {
  age: 12,
  bark: () => {},
  meow: () => {},
};

type test1 = Animal1 extends Cat ? true : false; // true
type test2 = Animal1 extends Dog ? true : false; // true

// union types
type Animal2 = Dog | Cat;

const unknownAnimal2: Animal2 = {
  age: 4,
  bark: () => {},
  meow: () => {},
};

type test3 = Animal2 extends Cat ? true : false; // false
type test4 = Animal2 extends Dog ? true : false; // false

type test5 = Cat extends Animal2 ? true : false; // true
type test6 = Dog extends Animal2 ? true : false; // true

通过 test1~4 的结果可以得知,经过 intersection 操作得到的 Animal1 既是 Cat 也是 Dog ,而经过 union 操作得到的 Animal2 既不是 Dog 也不是 。 从集合的角度来讲,intersection 是交集,得到的结果(也是一个集合)即是可以说的 A 也可以说是 B是一个子类型;union 是并集,得到的结果 C(集合)显然不能说是 A 或者 B,反而可以说 AB 都是 C

此时声明一个函数接受 Animal1 类型的参数,可以看到传入 unknownAnimal2 是不兼容的:

declare function nameAnimal(a: Animal1): void;

// const unknownAnimal2: Animal2
// Argument of type 'Animal2' is not assignable to parameter of type 'Animal1'.
//   Type 'Dog' is not assignable to type 'Animal1'.
//     Property 'meow' is missing in type 'Dog' but required in type 'Cat'.(2345)
// input.tsx(8, 3): 'meow' is declared here.
nameAnimal(unknownAnimal2);

再定义一个 unknownAnimal3 不显式地给它指定类型,反而是可以作为参数传递给 nameAnimal 的 (TypeScript is structural type system):

const unknownAnimal3 = {
  age: 4,
  bark: () => {},
  meow: () => {},
};

// 通过
nameAnimal(unknownAnimal3);

通过 extends 扩展 CatDog,得到的 Animal3 反而是和 Aniaml2(交叉类型)等价:

interface Animal3 extends Cat, Dog {} // equals to Cat & Dog

type test7 = Animal2 extends Animal1 ? true : false; // false
type test8 = Animal3 extends Animal2 ? true : false; // true
type test9 = Animal2 extends Animal3 ? true : false; // true

示例 2:当 A 和 B 的属性的类型「存在」矛盾时

标题部分 示例 2:当 A 和 B 的属性的类型「存在」矛盾时

playground link

interface Dog {
  age: string;
  bark: Function;
}

interface Cat {
  age: number;
  meow: Function;
}

// intersection types (is subtyping of both `Cat` and `Dog`)
type Animal1 = Dog & Cat;

const unknownAnimal1: Animal1 = {
  age: 12, // Type 'number' is not assignable to type 'never'.(2322)
  bark: () => {},
  meow: () => {},
};

const unknownAnimal2: Animal1 = {
  age: "12", // Type 'string' is not assignable to type 'never'.(2322)
  bark: () => {},
  meow: () => {},
};

type test1 = Animal1 extends Cat ? true : false; // true
type test2 = Animal1 extends Dog ? true : false; // true

// union types
type Animal2 = Dog | Cat;

const unknownAnimal3: Animal2 = {
  age: 4,
  bark: () => {},
  meow: () => {},
};

const unknownAnimal4: Animal2 = {
  age: "4",
  bark: () => {},
  meow: () => {},
};

type test3 = Animal2 extends Cat ? true : false; // false
type test4 = Animal2 extends Dog ? true : false; // false

Dog 的 age 是 string, Cat 的 age 是 number ,二者存在冲突。 在 intersection 得到的 Animal1 中,age 是 never,不管是 number 还是 string 都会报错。 而 union 得到的 Animal2 中,age 既可以是 number 又可以是 string

playground link

type A = "random" | "child";

type B = "hello" | "world";

type C = A | B;

type test1 = C extends "random" ? true : false; // false

type test2 = C extends "random" | "child" | "hello" | "world" ? true : false; // true

type test3 = C extends "random" | "child" ? true : false; // false

type D = A & B; // never

业务中常有这样的需求,比如接口返回成功时,code 字段是 0,数据是一个类型,错误时 code 字段是 -1 ,有一个错误信息字段 message。 这时候可以用 Discriminated Union 就有 用武之地了。不过可惜的是:

When every type in a union contains a common property with literal types, TypeScript considers that to be a discriminated union, and can narrow out the members of the union.

这里只能是 literal types

declare function fetchInstance<T>():
  | {
      code: 0;
      data: T;
    }
  | {
      code: -1; // Exclude<number, 0> 是行不通的。只能是 literal types
      message: string;
    };

const result = fetchInstance<number[]>()

if (result.code ===0) {
    console.log(result.data) // no message field
} else {
    console.log(result.message) // no data field
}
  1. What does the ampersand (&) mean in a TypeScript type definition?
  2. The Art of Type Programming
  3. 复制特殊符号的网站