引言
- 是否思考过交叉类型中“交叉”一词究竟代表什么?为什么叫交叉类型?
- 参考以下示例:
interface A {
x: number;
y: number;
}
interface B {
y: number;
z: number;
}
type U = A & B;
type I = A | B;
具体使用属性时,会发现ts的提示中,交叉类型 U
具有 x,y,z 三个属性,而联合类型 I
仅仅只有 y 属性,这个“联合”体现在哪里?
- 本文尝试从集合角度(个人思考),理解其设计的原则和模式
正文
类型和集合
这里需要重拾一下什么是“类型”。抛开“实例化”这些术语,从集合角度出发,类型应该是指 具有特定特性的若干值的集合。
如在Typescript中,number
类型是所有数字的集合,string
类型是所有字符串的集合,字面量类型 1
是只
包含一个元素 1 的集合,字面量类型 "1"
是只包含一个元素 "1" 的集合。
但是在对象中,集合的概念就非常容易混淆了,我以前一直不能理解为什么说 子类是父类的子集 ,明明子类会比父类拥有更多的属性和方法,难道不是父类是子类的子集?
这里是我一直以来都陷入的误区。看下面例子:
interface A {
x: number;
}
interface B extends A {
y: number;
}
const obj = {
x: 1,
xx: 2,
xxx: 3,
};
const a: A = obj;
const b: B = obj; // error
显然,变量 a
是 A
类型,但是实际上它所代表的对象,具有 x,xx,xxx 三个属性。显然,这里可以得出一个重要观点:对象类型只要求它所描述的对象 有 这些属性,而不一定是 有且仅有 这些属性。
在这个基础上,不难理解类型 B
是类型 A
的子集,因为 A
是所有具有 x
属性的对象的集合(有就行,对象可能有其他任意属性),而 B
除了要求对象有 x
属性,还要有一个 y
属性,要求明显比 A
更严格,套入集合的观点,明显 B
是 A
的子集。
即 对象类型是若干对象的集合,而不是属性的集合。只要一个对象具有它所描述的全部属性,那么该对象就是该类型的一员
注:Typescript并不完全基于集合的数学定义。为了实际的类型安全,如果改成用字面量对象赋值,将会报错
const a: A = {
x: 1,
xx: 2, // ERROR
xxx: 3, // ERROR
};
交集类型和并集类型运算符的简单运算
交叉类型即 Intersection types ,从当前文章角度,翻译它为 交集类型 可能会更加合适。
联合类型即 Union types,从当前文章角度,翻译它为 并集类型 可能会更加合适。
从集合角度,&
,|
运算符的各种运算结果:
type A = 1 | 2 | 3;
type B = 2 | 3 | 4;
type C = A & B; // C为: 2 | 3
type D = A & number; // D为: 1 | 2 | 3
type E = A | number; // E为: number
type F = number & string; // F为: never
类型 A-E
套一下集合运算即可。至于类型 F
, 虽然官方对 never
的讨论定义只是 “不会出现的类型” ,但我觉得其实是 “空集” 的思想在潜移默化的影响他们,把 never
看作是空集就完事了。
至于官方给到的几个运算特性,估计也是集合的思想在影响着,这些运算特性是显然易见的:
对于交集运算符 &
:
- 唯一性:
A & A
等价于A
. - 满足交换律:
A & B
等价于B & A
(函数调用和构造函数有些许不同,下面会讲). - 满足结合律:
(A & B) & C
等价于A & (B & C)
. - 父类型收敛: 当且仅当
B
是A
的父类型时,A & B
等价于A
.
对于并集运算符 |
:
- 唯一性:
A | A
等价于A
. - 满足交换律:
A | B
等价于B | A
. - 满足结合律:
(A | B) | C
等价于A | (B | C)
. - 子类型收敛: 当且仅当
B
是A
的子类型时,A | B
等价于A
.
注:Typescript的用词是 Supertype 和 Subtype,而不是 Superclass 和 Subclass 。也就是 A
和 B
并不一定是 Class
也可能是type
或 interface
,相信你能理解这些名词的差异。
交集类型和并集类型运算符的高级运算
Intersection types 交集类型:
&
比|
有更高的运算优先级- 如果类型
A
的属性p
是类型X
,类型B
的属性p
是类型Y
,那么交集类型A & B
中对应的属性p
是X & Y
- 交集类型
A & B
具有类型A
和B
的所有属性(即 属性的并集 )。再次强调,交集类型 不是属性集合的交集,而是 对象集合的交集。(类型的属性越多,代表限制条件越多,对象只有满足这些条件的并集,才能归为该交集类型) - 如果
A
和B
是函数类型,那么A & B
代表重载函数,此时集合运算顺序即为重载函数的函数签名顺序(如vscode智能提示中,第一个提示的重载函数类型会是A
类型) - 如果
A
是对象类型,B
是原始数据类型(string
,number
这些),代表“特殊tag”的原始数据类型
最后两点可能要结合实际代码理解:
- 函数的交集类型是重载函数:
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;
}
let m: M = overload; // OK
let n: N = overload; // OK
let mn: M & N = overload; // OK
显然类型 M
描述的函数是:能够 接受一个 number
并返回 number
。注意是 能够,而不是 能且只能,和前面的对象类型有异曲同工之妙。即overload
满足类型 M
是集合 M
的一员,也满足类型 N
是集合 N
的一员,所以它必然是 M & N
这个交集中的元素。
因此最终表现形式会是:函数类型的交集类型是重载函数
- 虽然对象类型和原始数据类型的交集,按集合逻辑应该是空集,但在ts中最终表现为 装箱 后与对象类型的属性并集。这实际上完全是为了另一个目的:nominal,即使得“类型别名”(
type
)具有唯一性(或者叫tag),官方FAQ有提及
// 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
// OK
let xs: string = x;
let ys: string = y;
xs = ys;
这在某些场景下有意义:A函数实际接受一个字符串,但只想接受B函数处理过的字符串作为参数。那么可以利用此特性实现:
function B(str:string){
return str as string & {__SPEC__: 'SPEC'}
}
function A(str: string & {__SPEC__: 'SPEC'}){...}
Union types 并集类型:
- 如果类型
A
的属性p
是类型X
,类型B
的属性p
是类型Y
,那么并集类型A | B
中对应的属性p
是X | Y
- 并集类型
A | B
一定具有 类型A
和B
的重复属性(即 属性的交集 )。这里也需要从实际出发,从类型安全角度,在不知道交集中的某个元素具体属于A
还是B
的情况下,访问时 TS只能给你提示它们一定都有的属性,因此表现上会是交集;赋值 时不多说了,满足A
或B
就行,标准的 并集 概念。 - 函数类型同样表现为并集的概念,不需要特别理解。唯一值得注意的地方是,在TS确实无法推测函数类型的情况下,调用这个函数:逆变 位置(参数)取交集类型,协变 位置(返回值)取并集类型。
注: 协变与逆变
后记
交叉类型的讨论:github.com/microsoft/T…
交叉类型的PR:github.com/microsoft/T…
联合类型的讨论:github.com/microsoft/T…
联合类型的PR:github.com/microsoft/T…
关于nominal的讨论: github.com/microsoft/T…
🤔还有个坑没填,所以这个“交叉”是...?
😂毕竟这里是TS,不是数学领域,当术语吧