接口

类似于对象类型字面量,接口类型也能够表示任意的对象类型。不同的是,接口类型能够给对象类型命名以及定义类型参数。接口类型无法表示原始类型,如 boolean 类型等。

接口声明只存在于编译阶段,在编译后生成的 JavaScript 代码中不包含任何接口代码。

接口声明

通过接口声明能够定义一个接口类型。接口声明的基础语法如下所示:

interface InterfaceName
{
    TypeMember;
    TypeMember;
    ...
}

在该语法中,interface 是关键字,InterfaceName 表示接口名,它必须是合法的标识符,TypeMember 表示接口的类型成员,所有类型成员都置于一对大括号 “{}” 之内。

按照惯例,接口名的首字母需要大写。因为接口定义了一种类型,而类型名的首字母通常需要大写。示例如下:

interface Shape { }

在接口名之后,由一对大括号 “{}” 包围起来的是接口类型中的类型成员。这部分的语法与 5.11.3 节中介绍的对象类型字面量的语法完全相同。从语法的角度来看,接口声明就是在对象类型字面量之前添加了 interface 关键字和接口名。因此,在 5.11.3 节中介绍的语法规则同样适用于接口声明。例如,类型成员间的分隔符和类型成员的尾后分号、逗号。

同样地,接口类型的类型成员也分为以下五类:

  • 属性签名

  • 调用签名

  • 构造签名

  • 方法签名

  • 索引签名

在 5.11.3 节中,我们详细介绍了属性签名。在 5.12 节中,我们详细介绍了调用签名和构造签名。这三种类型成员同样适用于接口类型。下面我们将简要回顾一下属性签名、调用签名和构造签名的语法,并着重介绍索引签名和方法签名。

属性签名

属性签名声明了对象类型中属性成员的名称和类型。属性签名的语法如下所示:

PropertyName: Type;

在该语法中,PropertyName 表示对象属性名,可以为标识符、字符串、数字和可计算属性名;Type 表示该属性的类型。示例如下:

interface Point {
    x: number;
    y: number;
}

关于属性签名的详细介绍请参考 5.11.3 节。

调用签名

调用签名定义了该对象类型表示的函数在调用时的类型参数、参数列表以及返回值类型。调用签名的语法如下所示:

(ParameterList): Type

在该语法中,ParameterList 表示函数形式参数列表类型;Type 表示函数返回值类型,两者都是可选的。示例如下:

interface ErrorConstructor {
    (message?: string): Error;
}

关于调用签名的详细介绍请参考 5.12.8 节。

构造签名

构造签名定义了该对象类型表示的构造函数在使用 new 运算符调用时的参数列表以及返回值类型。构造签名的语法如下所示:

new (ParameterList): Type

在该语法中,new 是运算符关键字;ParameterList 表示构造函数形式参数列表类型;Type 表示构造函数返回值类型,两者都是可选的。示例如下:

interface ErrorConstructor {
    new (message?: string): Error;
}

关于调用签名的详细介绍请参考 5.12.10 节。

方法签名

方法签名是声明函数类型的属性成员的简写。方法签名的语法如下所示:

PropertyName(ParameterList): Type

在该语法中,PropertyName 表示对象属性名,可以为标识符、字符串、数字和可计算属性名;ParameterList 表示可选的方法形式参数列表类型;Type 表示可选的方法返回值类型。从语法的角度来看,方法签名是在调用签名之前添加一个属性名作为方法名。

下例中定义了 Document 接口,它包含一个方法签名类型成员。该方法的方法名为 getElementById,它接受一个 string 类型的参数并返回 HTMLElement | null 类型的值。示例如下:

interface Document {
    getElementById(elementId: string): HTMLElement | null;
}

之所以说方法签名是声明函数类型的属性成员的简写,是因为方法签名可以改写为具有同等效果但语法稍显复杂的属性签名。我们知道方法签名的语法如下所示:

PropertyName(ParameterList): Type

将上述方法签名改写为具有同等效果的属性签名,如下所示:

PropertyName: { (ParameterList): Type }

在改写后的语法中,属性名保持不变并使用对象类型字面量和调用签名来表示函数类型。由于该对象类型字面量中仅包含一个调用签名,因此也可以使用函数类型字面量来代替对象类型字面量。示例如下:

PropertyName: (ParameterList) => Type

下面我们通过一个真实的例子来演示这三种可以互换的接口定义方式:

interface A {
    f(x: boolean): string;       // 方法签名
}

interface B {
    f: { (x: boolean): string }; // 属性签名和对象类型字面量
}

interface C {
    f: (x: boolean) => string;   // 属性签名和函数类型字面量
}

此例中我们定义了三个接口 ABC,它们都表示同一种类型,即定义了方法 f 的对象类型,方法 f 接受一个 boolean 类型的参数并返回 string 类型的值。

方法签名中的属性名可以为可计算属性名,这一点与属性签名中属性名的规则是相同的。关于可计算属性名规则的详细介绍请参考 5.11.3 节。示例如下:

const f = 'f';

interface A {
    [f](x: boolean): string;
}

若接口中包含多个名字相同但参数列表不同的方法签名成员,则表示该方法是重载方法。例如,下例中的方法 f 是一个重载方法,它具有三种调用签名:

interface A {
    f(): number;
    f(x: boolean): boolean;
    f(x: string, y: string): string;
}

索引签名

JavaScript 支持使用索引去访问对象的属性,即通过方括号 [] 语法去访问对象属性。一个典型的例子是数组对象,我们既可以使用数字索引去访问数组元素,也可以使用字符串索引去访问数组对象上的属性和方法。示例如下:

const colors = ['red', 'green', 'blue'];

// 访问数组中的第一个元素
const red = colors[0];

// 访问数组对象的length属性
const len = colors['length'];

接口中的索引签名能够描述使用索引访问的对象属性的类型。索引签名只有以下两种:

  • 字符串索引签名。

  • 数值索引签名。

字符串索引签名

字符串索引签名的语法如下所示:

[IndexName: string]: Type

在该语法中,IndexName 表示索引名,它可以为任意合法的标识符。索引名只起到占位的作用,它不代表真实的对象属性名;在字符串索引签名中,索引名的类型必须为 string 类型;Type 表示索引值的类型,它可以为任意类型。示例如下:

interface A {
    [prop: string]: number;
}

一个接口中最多只能定义一个字符串索引签名。字符串索引签名会约束该对象类型中所有属性的类型。例如,下例中的字符串索引签名定义了索引值的类型为 number 类型。那么,该接口中所有属性的类型必须能够赋值给 number 类型。示例如下:

interface A {
    [prop: string]: number;

    a: number;
    b: 0;
    c: 1 | 2;
}

此例中,属性 abc 的类型都能够赋值给字符串索引签名中定义的 number 类型,因此不会产生错误。接下来,我们再来看一个错误的例子:

interface B {
    [prop: string]: number;

    a: boolean;      // 编译错误
    b: () => number; // 编译错误
    c(): number;     // 编译错误
}

此例中,字符串索引签名中定义的索引值类型依旧为 number 类型。属性 a 的类型为 boolean 类型,它不能赋值给 number 类型,因此产生编译错误。属性 b 和方法 c 的类型均为函数类型,不能赋值给 number 类型,因此也会产生编译错误。

数值索引签名

数值索引签名的语法如下所示:

[IndexName: number]: Type

在该语法中,IndexName 表示索引名,它可以为任意合法的标识符。索引名只起到占位的作用,它不代表真实的对象属性名;在数值索引签名中,索引名的类型必须为 number 类型;Type 表示索引值的类型,它可以为任意类型。示例如下:

interface A {
    [prop: number]: string;
}

一个接口中最多只能定义一个数值索引签名。数值索引签名约束了数值属性名对应的属性值的类型。示例如下:

interface A {
    [prop: number]: string;
}

const obj: A = ['a', 'b', 'c'];

obj[0];  // string

若接口中同时存在字符串索引签名和数值索引签名,那么数值索引签名的类型必须能够赋值给字符串索引签名的类型。因为在 JavaScript 中,对象的属性名只能为字符串(或 Symbol)。虽然 JavaScript 也允许使用数字等其他值作为对象的索引,但最终它们都会被转换为字符串类型。因此,数值索引签名能够表示的属性集合是字符串索引签名能够表示的属性集合的子集。

下例中,字符串索引签名的类型为 number 类型,数值索引签名的类型为数字字面量联合类型 0 | 1。由于 0 | 1 类型能够赋值给 number 类型,因此该接口定义是正确的。示例如下:

interface A {
    [prop: string]: number;
    [prop: number]: 0 | 1;
}

但如果我们交换字符串索引签名和数值索引签名的类型,则会产生编译错误。示例如下:

interface A {
    [prop: string]: 0 | 1;
    [prop: number]: number; // 编译错误
}

可选属性与方法

在默认情况下,接口中属性签名和方法签名定义的对象属性都是必选的。在给接口类型赋值时,如果未指定必选属性则会产生编译错误。示例如下:

interface Foo {
    x: string;
    y(): number;
}

const a: Foo = { x: 'hi' };
//    ~
//    编译错误!缺少属性 'y'

const b: Foo = { y() { return 0; } };
//    ~
//    编译错误!缺少属性 'x'

// 正确
const c: Foo = {
    x: 'hi',
    y() { return 0; }
};

我们可以在属性名或方法名后添加一个问号 “?”,从而将该属性或方法定义为可选的。可选属性签名和可选方法签名的语法如下所示:

PropertyName?: Type

PropertyName?(ParameterList): Type

下例中,接口 Foo 的属性 x 和方法 y 都是可选的:

interface Foo {
    x?: string;
    y?(): number;
}

const a: Foo = {}
const b: Foo = { x: 'hi' }
const c: Foo = { y() { return 0; } }
const d: Foo = { x: 'hi', y() { return 0; } }

关于可选属性的详细介绍请参考 5.11.3 节。

如果接口中定义了重载方法,那么所有重载方法签名必须同时为必选的或者可选的。示例如下:

// 正确
interface Foo {
    a(): void;
    a(x: boolean): boolean;

    b?(): void;
    b?(x: boolean): boolean;
}

interface Bar {
    a(): void;
    a?(x: boolean): boolean;
//  ~
//  编译错误:重载签名必须全部为必选的或可选的
}

只读属性与方法

在接口声明中,使用 readonly 修饰符能够定义只读属性。readonly 修饰符只允许在属性签名和索引签名中使用,具体语法如下所示:

readonly PropertyName: Type;

readonly [IndexName: string]: Type
readonly [IndexName: number]: Type

例如,下例的接口 A 中定义了只读属性 a 和只读的索引签名:

interface A {
    readonly a: string;
    readonly [prop: string]: string;
    readonly [prop: number]: string;
}

若接口中定义了只读的索引签名,那么接口类型中的所有属性都是只读属性。示例如下:

interface A {
    readonly [prop: string]: number;
}

const a: A = { x: 0 };

a.x = 1; // 编译错误!不允许修改属性值

如果接口中既定义了只读索引签名,又定义了非只读的属性签名,那么非只读的属性签名定义的属性依旧是非只读的,除此之外的所有属性都是只读的。例如,下例的接口 A 中定义了只读索引签名和非只读属性 x。最终的结果为,属性 x 是非只读的,其余的属性为只读属性。示例如下:

interface A {
    readonly [prop: string]: number;
    x: number;
}

const a: A = { x: 0, y: 0 };

a.x = 1; // 正确
a.y = 1; // 错误

关于只读属性的详细介绍请参考 5.11.3 节。

接口的继承

接口可以继承其他的对象类型,这相当于将继承的对象类型中的类型成员复制到当前接口中。接口可以继承的对象类型如下:

  • 接口。

  • 对象类型的类型别名。

  • 类。

  • 对象类型的交叉类型。

本节将通过接口与接口之间的继承来介绍接口继承的具体使用方法。关于类型别名的详细介绍请参考 5.14 节。关于类的详细介绍请参考 5.15 节。关于交叉类型的详细介绍请参考 6.4 节。

接口的继承需要使用 extends 关键字。下例中,Circle 接口继承了 Shape 接口。我们可以将 Circle 接口称作子接口,同时将 Shape 接口称作父接口。示例如下:

interface Shape {
    name: string;
}

interface Circle extends Shape {
    radius: number;
}

一个接口可以同时继承多个接口,父接口名之间使用逗号分隔。下例中,Circle 接口同时继承了 Style 接口和 Shape 接口:

interface Style {
    color: string;
}

interface Shape {
    name: string;
}

interface Circle extends Style, Shape {
    radius: number;
}

当一个接口继承了其他接口后,子接口既包含了自身定义的类型成员,也包含了父接口中的类型成员。下例中,Circle 接口同时继承了 Style 接口和 Shape 接口,因此 Circle 接口中包含了 colornameradius 属性:

interface Style {
    color: string;
}

interface Shape {
    name: string;
}

interface Circle extends Style, Shape {
    radius: number;
}

const c: Circle = {
    color: 'red',
    name: 'circle',
    radius: 1
};

如果子接口与父接口之间存在同名的类型成员,那么子接口中的类型成员具有更高的优先级。同时,子接口与父接口中的同名类型成员必须是类型兼容的。也就是说,子接口中同名类型成员的类型需要能够赋值给父接口中同名类型成员的类型,否则将产生编译错误。示例如下:

interface Style {
    color: string;
}

interface Shape {
    name: string;
}

interface Circle extends Style, Shape {
    name: 'circle';

    color: number;
//  ~~~~~~~~~~~~~
//  编译错误:'color' 类型不兼容,
//  'number' 类型不能赋值给 'string' 类型
}

此例中,Circle 接口同时继承了 Style 接口和 Shape 接口。Circle 接口与父接口之间存在同名的属性 namecolorCircle 接口中 name 属性的类型为字符串字面量类型 'circle',它能够赋值给 Shape 接口中 string 类型的 name 属性,因此是正确的。而 Circle 接口中 color 属性的类型为 number,它不能够赋值给 Style 接口中 string 类型的 color 属性,因此产生编译错误。

如果仅是多个父接口之间存在同名的类型成员,而子接口本身没有该同名类型成员,那么父接口中同名类型成员的类型必须是完全相同的,否则将产生编译错误。示例如下:

interface Style {
    draw(): { color: string };
}

interface Shape {
    draw(): { x: number; y: number };
}

interface Circle extends Style, Shape {}
//        ~~~~~~
//        编译错误

此例中,Circle 接口同时继承了 Style 接口和 Shape 接口。Style 接口和 Shape 接口都包含一个名为 draw 的方法,但两者的返回值类型不同。当 Circle 接口尝试将两个 draw 方法合并时发生冲突,因此产生了编译错误。

解决这个问题的一个办法是,在 Circle 接口中定义一个同名的 draw 方法。这样 Circle 接口中的 draw 方法会拥有更高的优先级,从而取代父接口中的 draw 方法。这时编译器将不再进行类型合并操作,因此也就不会发生合并冲突。但是要注意,Circle 接口中定义的 draw 方法一定要与所有父接口中的 draw 方法是类型兼容的。示例如下:

interface Style {
    draw(): { color: string };
}

interface Shape {
    draw(): { x: number; y: number };
}

interface Circle extends Style, Shape {
    draw(): { color: string; x: number; y: number };
}

此例中,Circle 接口中定义了一个 draw 方法,它的返回值类型为 “{ color: string;x: number; y: number }”。它既能赋值给 “{ color: string }” 类型,也能赋值给 “{ x: number; y: number }” 类型,因此不会产生编译错误。

关于类型兼容性的详细介绍请参考 7.1 节。