TypeScript 联合、交叉类型 & 代数类型
首先要说明的是 TypeScript 是一个 structural type system,与之相对应的是 nominal type system(采用的语言有 C++/Java/C#/Rust 等)。二者的具体区别可以查看维基百科。
type
vs interface
标题部分 type vs interface二者的确十分相似,文档上说:
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 intype
, 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 总是可扩展的。
文档上比较有代表性的点:
- Type aliases may not participate in declaring merging, but interfaces can.
- Interfaces may only be used to declare the shapes of objects, not rename primitives.
建议查看官方文档上的 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 种类型
any
vs never
vs unkown
标题部分 any vs never vs unkown在 TypeSript 中存在 3 个特殊的类型,any
unknown
never
,他们的区别如下:
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 (联合类型)TLDR
标题部分 TLDRintersection 得到的是 子类型,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 | B
和 A & B
的区别。
示例 1:当 A 和 B 的属性的类型「不存在」矛盾时
标题部分 示例 1:当 A 和 B 的属性的类型「不存在」矛盾时interface Dog { age: number; bark: Function;}
interface Cat { age: number; meow: Function;}
// intersection typestype Animal1 = Dog & Cat;
const unknownAnimal1: Animal1 = { age: 12, bark: () => {}, meow: () => {},};
type test1 = Animal1 extends Cat ? true : false; // truetype test2 = Animal1 extends Dog ? true : false; // true
// union typestype Animal2 = Dog | Cat;
const unknownAnimal2: Animal2 = { age: 4, bark: () => {}, meow: () => {},};
type test3 = Animal2 extends Cat ? true : false; // falsetype test4 = Animal2 extends Dog ? true : false; // false
type test5 = Cat extends Animal2 ? true : false; // truetype test6 = Dog extends Animal2 ? true : false; // true
通过 test1~4 的结果可以得知,经过 intersection 操作得到的 Animal1 既是 Cat
也是 Dog
,而经过 union 操作得到的 Animal2 既不是 Dog 也不是 。
从集合的角度来讲,intersection 是交集,得到的结果(也是一个集合)即是可以说的 A
也可以说是 B
,是一个子类型;union 是并集,得到的结果 C
(集合)显然不能说是 A
或者 B
,反而可以说
A
、B
都是 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 扩展 Cat
和 Dog
,得到的 Animal3
反而是和 Aniaml2
(交叉类型)等价:
interface Animal3 extends Cat, Dog {} // equals to Cat & Dog
type test7 = Animal2 extends Animal1 ? true : false; // falsetype test8 = Animal3 extends Animal2 ? true : false; // truetype test9 = Animal2 extends Animal3 ? true : false; // true
示例 2:当 A 和 B 的属性的类型「存在」矛盾时
标题部分 示例 2:当 A 和 B 的属性的类型「存在」矛盾时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; // truetype test2 = Animal1 extends Dog ? true : false; // true
// union typestype Animal2 = Dog | Cat;
const unknownAnimal3: Animal2 = { age: 4, bark: () => {}, meow: () => {},};
const unknownAnimal4: Animal2 = { age: "4", bark: () => {}, meow: () => {},};
type test3 = Animal2 extends Cat ? true : false; // falsetype test4 = Animal2 extends Dog ? true : false; // false
Dog
的 age 是 string
, Cat
的 age 是 number
,二者存在冲突。
在 intersection 得到的 Animal1 中,age 是 never
,不管是 number
还是 string
都会报错。
而 union 得到的 Animal2 中,age 既可以是 number
又可以是 string
。
其它的一些例子
标题部分 其它的一些例子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
Discriminated Union
标题部分 Discriminated Union业务中常有这样的需求,比如接口返回成功时,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}