局部类型、联合类型、交叉类型
1、局部类型
TypeScript同样支持声明具有块级作用域的局部类型,主要包括:
- 局部枚举类型。
- 局部类类型。
- 局部接口类型。
- 局部类型别名。
下例中在函数f内部分别声明了以上几种局部类型:
ts
function f<T>() {
enum E {
A,
B,
}
class C {
x: string | undefined;
}
//允许带有泛型参数
interface I<T> {
x: T;
}
//可以引用其他局部类型
type A = E.A | E.B;
}
此例中,枚举类型E、类类型C、接口类型I和类型别名A都是局部类型。局部类型也允许带有类型参数,并且可以引用外层作用域中的类型参数。
类似于let声明和const声明,局部类型拥有块级作用域。例如,下例中在if分支和else支持中均声明了接口T,它们仅在各自所处的块级作用域内生效。因此,这两个接口T不会相互影响,并且if分支中的代码也无法引用else分支中的接口T。示例如下:
ts
function f(x: boolean) {
if (x) {
interface T {
x: number;
}
const v: T = { x: 0 };
} else {
interface T {
x: string;
}
const v: T = { x: 'foo' };
}
}
2、联合类型
联合类型由一组有序的成员类型构成。联合类型表示一个值的类型可以为若干种类型之一。例如,联合类型"string | number"表示一个值的类型既可以为string类型也可以为number类型。
联合类型通过联合类型字面量来定义。
2.1、联合类型字面量
联合类型由两个或两个以上的成员类型构成,各成员类型之间使用竖线符号"|"分隔。示例如下:
ts
type NumericType = number | bigint;
此例中,定义了一个名为NumericType的联合类型。该联合类型由两个成员类型组成,即number类型和bigint类型。若一个值既可能为number类型又可能为bigint类型,那么我们说该值的类型为联合类型"number | bigint"。
联合类型的成员类型可以为任意类型,如原始类型、数组类型、对象类型,以及函数类型等。示例如下:
ts
type T = boolean | string[] | { x: number } | (() => void);
如果联合类型中存在相同的成员类型,那么相同的成员类型将被合并为单一成员类型。例如,下例中的类型别名T0和T1都表示boolean类型,类型别名T2和T3都表示同一种联合类型"boolean |string":
ts
type T0 = boolean; // boolean
type T1 = boolean | boolean; // boolean
type T2 = boolean | string; // boolean | string
type T3 = boolean | string | boolean; // boolean | string
联合类型是有序的成员类型的集合。在绝大部分情况下,成员类型满足类似于数学中的"加法交换律",即改变成员类型的顺序不影响联合类型的结果类型。下例中,虽然联合类型T0与T1中成员类型的顺序不同,但是T0与T1表示相同的类型:
ts
type T0 = string | number;
type T1 = number | string;
联合类型中的类型成员同样满足类似于数学中的"加法结合律"。对部分类型成员使用分组运算符不影响联合类型的结果类型。例如,下例中的T0和T1类型是同一个类型:
ts
type T0 = (boolean | string) | number;
type T1 = boolean | (string | number);
联合类型的成员类型可以进行化简。假设有联合类型"U = T0 |T1",如果T1是T0的子类型,那么可以将类型成员T1从联合类型U中消去。最后,联合类型U的结果类型为"U = T0"。例如,有联合类型"boolean | true | false"。其中,true类型和false类型是boolean类型的子类型,因此可以将true类型和false类型从联合类型中消去。最终,联合类型"boolean | true | false"的结果类型为boolean类型。示例如下:
ts
// 'true' 和 'false' 类型是 'boolean' 类型的子类型
type T0 = boolean | true | false;
// 所以T0等同于 T1
type T1 = boolean;
2.2、联合类型的类型成员
像接口类型一样,联合类型作为一个整体也可以有类型成员,只不过联合类型的类型成员是由其成员类型决定的。
2.2.1、属性签名
若联合类型U中的每个成员类型都包含一个同名的属性签名M,那么联合类型U也包含属性签名M。例如,有如下定义的Circle类型与Rectangle类型,以及由这两个类型构成的联合类型Shape:
ts
interface Circle {
area: number;
radius: number;
}
interface Rectangle {
area: number;
width: number;
height: number;
}
type Shape = Circle | Rectangle;
此例中,因为Circle类型与Rectangle类型均包含名为area的属性签名类型成员,所以联合类型Shape也包含名为area的属性签名类型成员。因此,允许访问Shape类型上的area属性。示例如下:
ts
type Shape = Circle | Rectangle;
declare const s: Shape;
s.area; // number
因为radius、width和height类型成员不是 Circle类型和Rectangle类型的共同类型成员,因此它们不是Shape联合类型的类型成员。示例如下:
ts
type Shape = Circle | Rectangle;
declare const s: Shape;
s.radius; // 错误
s.width; // 错误
s.height; // 错误
对于联合类型的属性签名,其类型为所有成员类型中该属性类型的联合类型。例如,下例中联合类型"Circle | Rectangle"具有属性签名area,其类型为Circle类型中area属性的类型和Rectangle类型中area属性的类型组成的联合类型,即"bigint | number"类型。示例如下:
ts
interface Circle {
area: bigint;
}
interface Rectangle {
area: number;
}
declare const s: Circle | Rectangle;
s.area; // bigint | number
如果联合类型的属性签名在某个成员类型中是可选属性签名,那么该属性签名在联合类型中也是可选属性签名;否则,该属性签名在联合类型中是必选属性签名。示例如下:
ts
interface Circle {
area: bigint;
}
interface Rectangle {
area?: number;
}
declare const s: Circle | Rectangle;
s.area; // bigint | number | undefined
此例中,area属性在Rectangle类型中是可选的属性。因此,在联合类型"Circle | Rectangle"中,area属性也是可选属性。
2.2.2、索引签名
索引签名包含两种,即字符串索引签名和数值索引签名。在联合类型中,这两种索引签名具有相似的行为。
如果联合类型中每个成员都包含字符串索引签名,那么该联合类型也拥有了字符串索引签名,字符串索引签名中的索引值类型为每个成员类型中索引值类型的联合类型;否则,该联合类型没有字符串索引签名。例如,下例中的联合类型T相当于接口类型T0T1:
ts
interface T0 {
[prop: string]: number;
}
interface T1 {
[prop: string]: bigint;
}
type T = T0 | T1;
interface T0T1 {
[prop: string]: number | bigint;
}
如果联合类型中每个成员都包含数值索引签名,那么该联合类型也拥有了数值索引签名,数值索引签名中的索引值类型为每个成员类型中索引值类型的联合类型;否则,该联合类型没有数值索引签名。例如,下例中的联合类型T相当于接口类型T0T1:
ts
interface T0 {
[prop: number]: number;
}
interface T1 {
[prop: number]: bigint;
}
type T = T0 | T1;
interface T0T1 {
[prop: number]: number | bigint;
}
2.2.3、调用签名与构造签名
如果联合类型中每个成员类型都包含相同参数列表的调用签名,那么联合类型也拥有了该调用签名,其返回值类型为每个成员类型中调用签名返回值类型的联合类型;否则,该联合类型没有调用签名。例如,下例中的联合类型T相当于接口类型T0T1:
ts
interface T0 {
(name: string): number;
}
interface T1 {
(name: string): bigint;
}
type T = T0 | T1;
interface T0T1 {
(name: string): number | bigint;
}
同理,如果联合类型中每个成员都包含相同参数列表的构造签名,那么该联合类型也拥有了构造签名,其返回值类型为每个成员类型中构造签名返回值类型的联合类型;否则,该联合类型没有构造签名。例如,下例中的联合类型T相当于接口类型T0T1:
ts
interface T0 {
new (name: string): Date;
}
interface T1 {
new (name: string): Error;
}
type T = T0 | T1;
interface T0T1 {
new (name: string): Date | Error;
}
3、交叉类型
交叉类型在逻辑上与联合类型是互补的。联合类型表示一个值的类型为多种类型之一,而交叉类型则表示一个值同时属于多种类型。
交叉类型通过交叉类型字面量来定义。
3.1、交叉类型字面量
交叉类型由两个或多个成员类型构成,各成员类型之间使用"&"符号分隔。示例如下:
ts
interface Clickable {
click(): void;
}
interface Focusable {
focus(): void;
}
type T = Clickable & Focusable;
此例中,定义了一个名为T的交叉类型。该交叉类型由两个成员类型组成,即Clickable类型和Focusable类型。若一个值既是Clickable类型又是Focusable类型,那么我们说该值的类型为交叉类型"Clickable & Focusable"。它表示既可以点击又可以获得焦点的对象。
3.1.1、成员类型的运算
与联合类型相似,如果交叉类型中存在多个相同的成员类型,那么相同的成员类型将被合并为单一成员类型。示例如下:
ts
type T0 = boolean;
type T1 = boolean & boolean;
type T2 = boolean & boolean & boolean;
此例中,T0、T1和T2都表示同一种类型boolean。
交叉类型是有序的成员类型的集合。在绝大部分情况下,成员类型满足类似于数学中的"加法交换律",即改变成员类型的顺序不影响交叉类型的结果类型。例如,下例中虽然交叉类型T0与T1中成员类型的定义顺序不同,但是T0与T1表示相同的类型:
ts
interface Clickable {
click(): void;
}
interface Focusable {
focus(): void;
}
type T0 = Clickable & Focusable;
type T1 = Focusable & Clickable;
需要注意的是,当交叉类型涉及调用签名重载或构造签名重载时便失去了"加法交换律"的性质。因为交叉类型中成员类型的顺序将决定重载签名的顺序,进而将影响重载签名的解析顺序。示例如下:
ts
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()"方法具有更高的优先级。此例也说明了调用签名重载的顺序与交叉类型中成员类型的定义顺序是一致的。
交叉类型中的类型成员同样满足类似于数学中的"加法结合律"。对部分类型成员使用分组运算符不影响交叉类型的结果类型。例如,下例中的T0和T1类型是同一种类型:
ts
interface Clickable {
click(): void;
}
interface Focusable {
focus(): void;
}
interface Scrollable {
scroll(): void;
}
type T0 = (Clickable & Focusable) & Scrollable;
type T1 = Clickable & (Focusable & Scrollable);
3.1.2、原生类型
交叉类型通常与对象类型一起使用。虽然在交叉类型中也允许使用原始类型成员,但结果类型将成为never类型,因此在实际代码中并不常见。示例如下:
ts
type T = boolean & number & string;
此例中,类型T是boolean、number和string类型组成的交叉类型。根据交叉类型的定义,若一个值是T类型,那么该值既是boolean类型,又是number类型,还是string类型。显然,不存在这样一个值,所以T类型为never类型。never类型是尾端类型,是一种不存在可能值的类型。
3.2、交叉类型的类型成员
3.2.1、属性签名
只要交叉类型I中任意一个成员类型包含了属性签名M,那么交叉类型I也包含属性签名M。例如,有以下的接口类型A和B:
ts
interface A {
a: boolean;
}
interface B {
b: string;
}
那么,接口类型A和B的交叉类型"A & B"为如下对象类型:
ts
{
a: boolean;
b: string;
}
对于交叉类型的属性签名,其类型为所有成员类型中该属性类型的交叉类型。例如,有以下接口类型A和B:
ts
interface A {
x: { a: boolean };
}
interface B {
x: { b: boolean };
}
那么,接口类型A和B的交叉类型"A & B"为如下对象类型:
ts
{
x: { a: boolean } & { b: boolean }
}
该类型也等同于如下类型:
ts
{
x: {
a: boolean;
b: boolean;
};
}
若交叉类型的属性签名M在所有成员类型中都是可选属性,那么该属性签名在交叉类型中也是可选属性。否则,属性签名M是一个必选属性。例如,有以下接口类型A和B:
ts
interface A {
x: boolean;
y?: string;
}
interface B {
x?: boolean;
y?: string;
}
那么,接口类型A和B的交叉类型"A & B"为如下对象类型:
ts
{
x: boolean;
y?: string;
}
在"A & B"交叉类型中,属性x是必选属性,属性y是可选属性。
3.2.2、索引签名
如果交叉类型中任何一个成员类型包含了索引签名,那么该交叉类型也拥有了索引签名;否则,该交叉类型没有索引签名。例如,有以下接口类型A和B:
ts
interface A {
[prop: string]: string;
}
interface B {
[prop: number]: string;
}
那么,接口类型A和B的交叉类型"A & B"为如下对象类型,它同时包含了字符串索引签名和数值索引签名:
ts
{
[prop: string]: string;
[prop: number]: string;
}
交叉类型索引签名中的索引值类型为每个成员类型中索引值类型的交叉类型。例如,有以下接口类型A和B:
ts
interface A {
[prop: string]: { a: boolean };
}
interface B {
[prop: string]: { b: boolean };
}
那么,接口类型A和B的交叉类型"A & B"为如下对象类型:
ts
{
[prop: string]: { a: boolean } & { b: boolean };
}
该类型也等同于如下类型:
ts
{
[prop: string]: {
a: boolean;
b: boolean;
};
}
3.2.3、调用签名与构造签名
若交叉类型的成员类型中含有调用签名或构造签名,那么这些调用签名和构造签名将以成员类型的先后顺序合并到交叉类型中。例如,有以下接口类型A和B:
ts
interface A {
(x: number): number;
}
interface B {
(x: string): string;
}
那么交叉类型"A & B"为如下对象类型:
ts
{
(x: boolean): boolean;
(x: string): string;
}
同时,交叉类型"B & A"为如下对象类型:
ts
{
(x: string): string;
(x: boolean): boolean;
}
通过这两个例子能够看到,交叉类型中调用签名的顺序与交叉类型类型成员的顺序相同,构造签名同理。当交叉类型中存在重载签名时,需要特别留意类型成员的定义顺序。
3.3、交叉类型与联合类型
3.3.1、优先级
当表示交叉类型的"&"符号与表示联合类型的"|"符号同时使用时,"&"符号具有更高的优先级。"&"符号如同数学中的乘法符号"×",而"|"符号则如同数学中的加法符号"+"。例如,有如下复合类型:
ts
A & B | C & D
该类型等同于如下类型:
ts
(A & B) | (C & D)
还要注意,当表示交叉类型的"&"符号与表示联合类型的"|"符号与函数类型字面量同时使用时,"&"符号和"|"符号拥有更高的优先级。例如,有如下类型:
ts
() => bigint | number
该类型等同于如下类型,即返回值类型为联合类型"bigint |number"的函数类型:
ts
() => (bigint | number)
而不是函数类型和number类型的联合类型,如下所示:
ts
(() => bigint) | number
在任何时候,我们都可以使用分组运算符"()"来明确指定优先级。
3.3.2、分配率性质
由交叉类型和联合类型组成的类型满足类似于数学中乘法分配律的规则。表示交叉类型的"&"符号如同数学中的乘法符号"×",而表示联合类型的"|"符号则如同数学中的加法符号"+"。下例中的"≡"符号是恒等号,表示符号两侧是恒等关系:
ts
A & (B | C) ≡ (A & B) | (A & C)
另一个稍微复杂的示例如下所示:
ts
(A | B) & (C | D) ≡ A & C | A & D | B & C | B & D
如果我们对照着数学中的"乘法分配律",那么此例中的结果将不难理解。
了解了交叉类型与联合类型的分配律性质后,我们就能够分析与理解一些复杂的类型。例如,有如下的复合类型:
ts
T = (string | 0) & (number | 'a');
利用上文介绍的规则将该类型展开就能得到最终的结果类型,如下所示:
ts
T = (string | 0) & (number | 'a');
= (string & number) | (string & 'a') | (0 & number) | (0 & 'a');
// 没有交集的原始类型的交叉类型是 'never' 类型
= never | 'a' | 0 | never;
// 'never' 尾端类型是所有类型的子类型
// 并且若某成员是其他成员的子类型,则可以从联合类型中消去
= 'a' | 0;