# TypeScript 的 interface 接口

网道（WangDoc.com），互联网文档计划

## 简介 #

interface 是对象的模板，可以看作是一种类型约定，中文译为“接口”。使用了某个模板的对象，就拥有了指定的类型结构。

    interface Person {
      firstName: string;
      lastName: string;
      age: number;
    }

上面示例中，定义了一个接口`Person`，它指定一个对象模板，拥有三个属性`firstName`、`lastName`和`age`。任何实现这个接口的对象，都必须部署这三个属性，并且必须符合规定的类型。

实现该接口很简单，只要指定它作为对象的类型即可。

    const p:Person = {
      firstName: 'John',
      lastName: 'Smith',
      age: 25
    };

上面示例中，变量`p`的类型就是接口`Person`，所以必须符合`Person`指定的结构。

方括号运算符可以取出 interface 某个属性的类型。

    interface Foo {
      a: string;
    }

    type A = Foo['a']; // string

上面示例中，`Foo['a']`返回属性`a`的类型，所以类型`A`就是`string`。

interface 可以表示对象的各种语法，它的成员有5种形式。

- 对象属性
- 对象的属性索引
- 对象方法
- 函数
- 构造函数

（1）对象属性

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

上面示例中，`x`和`y`都是对象的属性，分别使用冒号指定每个属性的类型。

属性之间使用分号或逗号分隔，最后一个属性结尾的分号或逗号可以省略。

如果属性是可选的，就在属性名后面加一个问号。

    interface Foo {
      x?: string;
    }

如果属性是只读的，需要加上`readonly`修饰符。

    interface A {
      readonly a: string;
    }

（2）对象的属性索引

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

上面示例中，`[prop: string]`就是属性的字符串索引，表示属性名只要是字符串，都符合类型要求。

属性索引共有`string`、`number`和`symbol`三种类型。

一个接口中，最多只能定义一个字符串索引。字符串索引会约束该类型中所有名字为字符串的属性。

    interface MyObj {
      [prop: string]: number;

      a: boolean;      // 编译错误
    }

上面示例中，属性索引指定所有名称为字符串的属性，它们的属性值必须是数值（`number`）。属性`a`的值为布尔值就报错了。

属性的数值索引，其实是指定数组的类型。

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

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

上面示例中，`[prop: number]`表示属性名的类型是数值，所以可以用数组对变量`obj`赋值。

同样的，一个接口中最多只能定义一个数值索引。数值索引会约束所有名称为数值的属性。

如果一个 interface 同时定义了字符串索引和数值索引，那么数值索引必须服从于字符串索引。因为在 JavaScript 中，数值属性名最终是自动转换成字符串属性名。

    interface A {
      [prop: string]: number;
      [prop: number]: string; // 报错
    }

    interface B {
      [prop: string]: number;
      [prop: number]: number; // 正确
    }

上面示例中，数值索引的属性值类型与字符串索引不一致，就会报错。数值索引必须兼容字符串索引的类型声明。

（3）对象的方法

对象的方法共有三种写法。

    // 写法一
    interface A {
      f(x: boolean): string;
    }

    // 写法二
    interface B {
      f: (x: boolean) => string;
    }

    // 写法三
    interface C {
      f: { (x: boolean): string };
    }

属性名可以采用表达式，所以下面的写法也是可以的。

    const f = 'f';

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

类型方法可以重载。

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

interface 里面的函数重载，不需要给出实现。但是，由于对象内部定义方法时，无法使用函数重载的语法，所以需要额外在对象外部给出函数方法的实现。

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

    function MyFunc(): number;
    function MyFunc(x: boolean): boolean;
    function MyFunc(x: string, y: string): string;
    function MyFunc(
      x?:boolean|string, y?:string
    ):number|boolean|string {
      if (x === undefined && y === undefined) return 1;
      if (typeof x === 'boolean' && y === undefined) return true;
      if (typeof x === 'string' && typeof y === 'string') return 'hello';
      throw new Error('wrong parameters');
    }

    const a:A = {
      f: MyFunc
    }

上面示例中，接口`A`的方法`f()`有函数重载，需要额外定义一个函数`MyFunc()`实现这个重载，然后部署接口`A`的对象`a`的属性`f`等于函数`MyFunc()`就可以了。

（4）函数

interface 也可以用来声明独立的函数。

    interface Add {
      (x:number, y:number): number;
    }

    const myAdd:Add = (x,y) => x + y;

上面示例中，接口`Add`声明了一个函数类型。

（5）构造函数

interface 内部可以使用`new`关键字，表示构造函数。

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

上面示例中，接口`ErrorConstructor`内部有`new`命令，表示它是一个构造函数。

TypeScript 里面，构造函数特指具有`constructor`属性的类，详见《Class》一章。

## interface 的继承 #

interface 可以继承其他类型，主要有下面几种情况。

### interface 继承 interface #

interface 可以使用`extends`关键字，继承其他 interface。

    interface Shape {
      name: string;
    }

    interface Circle extends Shape {
      radius: number;
    }

上面示例中，`Circle`继承了`Shape`，所以`Circle`其实有两个属性`name`和`radius`。这时，`Circle`是子接口，`Shape`是父接口。

`extends`关键字会从继承的接口里面拷贝属性类型，这样就不必书写重复的属性。

interface 允许多重继承。

    interface Style {
      color: string;
    }

    interface Shape {
      name: string;
    }

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

上面示例中，`Circle`同时继承了`Style`和`Shape`，所以拥有三个属性`color`、`name`和`radius`。

多重接口继承，实际上相当于多个父接口的合并。

如果子接口与父接口存在同名属性，那么子接口的属性会覆盖父接口的属性。注意，子接口与父接口的同名属性必须是类型兼容的，不能有冲突，否则会报错。

    interface Foo {
      id: string;
    }

    interface Bar extends Foo {
      id: number; // 报错
    }

上面示例中，`Bar`继承了`Foo`，但是两者的同名属性`id`的类型不兼容，导致报错。

多重继承时，如果多个父接口存在同名属性，那么这些同名属性不能有类型冲突，否则会报错。

    interface Foo {
      id: string;
    }

    interface Bar {
      id: number;
    }

    // 报错
    interface Baz extends Foo, Bar {
      type: string;
    }

上面示例中，`Baz`同时继承了`Foo`和`Bar`，但是后两者的同名属性`id`有类型冲突，导致报错。

### interface 继承 type #

interface 可以继承`type`命令定义的对象类型。

    type Country = {
      name: string;
      capital: string;
    }

    interface CountryWithPop extends Country {
      population: number;
    }

上面示例中，`CountryWithPop`继承了`type`命令定义的`Country`对象，并且新增了一个`population`属性。

注意，如果`type`命令定义的类型不是对象，interface 就无法继承。

### interface 继承 class #

interface 还可以继承 class，即继承该类的所有成员。关于 class 的详细解释，参见下一章。

    class A {
      x:string = '';

      y():boolean {
        return true;
      }
    }

    interface B extends A {
      z: number
    }

上面示例中，`B`继承了`A`，因此`B`就具有属性`x`、`y()`和`z`。

实现`B`接口的对象就需要实现这些属性。

    const b:B = {
      x: '',
      y: function(){ return true },
      z: 123
    }

上面示例中，对象`b`就实现了接口`B`，而接口`B`又继承了类`A`。

某些类拥有私有成员和保护成员，interface 可以继承这样的类，但是意义不大。

    class A {
      private x: string = '';
      protected y: string = '';
    }

    interface B extends A {
      z: number
    }

    // 报错
    const b:B = { /* ... */ }

    // 报错
    class C implements B {
      // ...
    }

上面示例中，`A`有私有成员和保护成员，`B`继承了`A`，但无法用于对象，因为对象不能实现这些成员。这导致`B`只能用于其他 class，而这时其他 class 与`A`之间不构成父类和子类的关系，使得`x`与`y`无法部署。

## 接口合并 #

多个同名接口会合并成一个接口。

    interface Box {
      height: number;
      width: number;
    }

    interface Box {
      length: number;
    }

上面示例中，两个`Box`接口会合并成一个接口，同时有`height`、`width`和`length`三个属性。

这样的设计主要是为了兼容 JavaScript 的行为。JavaScript 开发者常常对全局对象或者外部库，添加自己的属性和方法。那么，只要使用 interface 给出这些自定义属性和方法的类型，就能自动跟原始的 interface 合并，使得扩展外部类型非常方便。

举例来说，Web 网页开发经常会对`window`对象和`document`对象添加自定义属性，但是 TypeScript 会报错，因为原始定义没有这些属性。解决方法就是把自定义属性写成 interface，合并进原始定义。

    interface Document {
      foo: string;
    }

    document.foo = 'hello';

上面示例中，接口`Document`增加了一个自定义属性`foo`，从而就可以在`document`对象上使用自定义属性。

同名接口合并时，同一个属性如果有多个类型声明，彼此不能有类型冲突。

    interface A {
      a: number;
    }

    interface A {
      a: string; // 报错
    }

上面示例中，接口`A`的属性`a`有两个类型声明，彼此是冲突的，导致报错。

同名接口合并时，如果同名方法有不同的类型声明，那么会发生函数重载。而且，后面的定义比前面的定义具有更高的优先级。

    interface Cloner {
      clone(animal: Animal): Animal;
    }

    interface Cloner {
      clone(animal: Sheep): Sheep;
    }

    interface Cloner {
      clone(animal: Dog): Dog;
      clone(animal: Cat): Cat;
    }

    // 等同于
    interface Cloner {
      clone(animal: Dog): Dog;
      clone(animal: Cat): Cat;
      clone(animal: Sheep): Sheep;
      clone(animal: Animal): Animal;
    }

上面示例中，`clone()`方法有不同的类型声明，会发生函数重载。这时，越靠后的定义，优先级越高，排在函数重载的越前面。比如，`clone(animal: Animal)`是最先出现的类型声明，就排在函数重载的最后，属于`clone()`函数最后匹配的类型。

这个规则有一个例外。同名方法之中，如果有一个参数是字面量类型，字面量类型有更高的优先级。

    interface A {
      f(x:'foo'): boolean;
    }

    interface A {
      f(x:any): void;
    }

    // 等同于
    interface A {
      f(x:'foo'): boolean;
      f(x:any): void;
    }

上面示例中，`f()`方法有一个类型声明的参数`x`是字面量类型，这个类型声明的优先级最高，会排在函数重载的最前面。

一个实际的例子是 Document 对象的`createElement()`方法，它会根据参数的不同，而生成不同的 HTML 节点对象。

    interface Document {
      createElement(tagName: any): Element;
    }
    interface Document {
      createElement(tagName: "div"): HTMLDivElement;
      createElement(tagName: "span"): HTMLSpanElement;
    }
    interface Document {
      createElement(tagName: string): HTMLElement;
      createElement(tagName: "canvas"): HTMLCanvasElement;
    }

    // 等同于
    interface Document {
      createElement(tagName: "canvas"): HTMLCanvasElement;
      createElement(tagName: "div"): HTMLDivElement;
      createElement(tagName: "span"): HTMLSpanElement;
      createElement(tagName: string): HTMLElement;
      createElement(tagName: any): Element;
    }

上面示例中，`createElement()`方法的函数重载，参数为字面量的类型声明会排到最前面，返回具体的 HTML 节点对象。类型越不具体的参数，排在越后面，返回通用的 HTML 节点对象。

如果两个 interface 组成的联合类型存在同名属性，那么该属性的类型也是联合类型。

    interface Circle {
      area: bigint;
    }

    interface Rectangle {
      area: number;
    }

    declare const s: Circle | Rectangle;

    s.area;   // bigint | number

上面示例中，接口`Circle`和`Rectangle`组成一个联合类型`Circle | Rectangle`。因此，这个联合类型的同名属性`area`，也是一个联合类型。本例中的`declare`命令表示变量`s`的具体定义，由其他脚本文件给出，详见《declare 命令》一章。

## interface 与 type 的异同 #

`interface`命令与`type`命令作用类似，都可以表示对象类型。

很多对象类型既可以用 interface 表示，也可以用 type 表示。而且，两者往往可以换用，几乎所有的 interface 命令都可以改写为 type 命令。

它们的相似之处，首先表现在都能为对象类型起名。

    type Country = {
      name: string;
      capital: string;
    }

    interface Country {
      name: string;
      capital: string;
    }

上面示例是`type`命令和`interface`命令，分别定义同一个类型。

`class`命令也有类似作用，通过定义一个类，同时定义一个对象类型。但是，它会创造一个值，编译后依然存在。如果只是单纯想要一个类型，应该使用`type`或`interface`。

interface 与 type 的区别有下面几点。

（1）`type`能够表示非对象类型，而`interface`只能表示对象类型（包括数组、函数等）。

（2）`interface`可以继承其他类型，`type`不支持继承。

继承的主要作用是添加属性，`type`定义的对象类型如果想要添加属性，只能使用`&`运算符，重新定义一个类型。

    type Animal = {
      name: string
    }

    type Bear = Animal & {
      honey: boolean
    }

上面示例中，类型`Bear`在`Animal`的基础上添加了一个属性`honey`。

上例的`&`运算符，表示同时具备两个类型的特征，因此可以起到两个对象类型合并的作用。

作为比较，`interface`添加属性，采用的是继承的写法。

    interface Animal {
      name: string
    }

    interface Bear extends Animal {
      honey: boolean
    }

继承时，type 和 interface 是可以换用的。interface 可以继承 type。

    type Foo = { x: number; };

    interface Bar extends Foo {
      y: number;
    }

type 也可以继承 interface。

    interface Foo {
      x: number;
    }

    type Bar = Foo & { y: number; };

（3）同名`interface`会自动合并，同名`type`则会报错。也就是说，TypeScript 不允许使用`type`多次定义同一个类型。

    type A = { foo:number }; // 报错
    type A = { bar:number }; // 报错

上面示例中，`type`两次定义了类型`A`，导致两行都会报错。

作为比较，`interface`则会自动合并。

    interface A { foo:number };
    interface A { bar:number };

    const obj:A = {
      foo: 1,
      bar: 1
    };

上面示例中，`interface`把类型`A`的两个定义合并在一起。

这表明，interface 是开放的，可以添加属性，type 是封闭的，不能添加属性，只能定义新的 type。

（4）`interface`不能包含属性映射（mapping），`type`可以，详见《映射》一章。

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

    // 正确
    type PointCopy1 = {
      [Key in keyof Point]: Point[Key];
    };

    // 报错
    interface PointCopy2 {
      [Key in keyof Point]: Point[Key];
    };

（5）`this`关键字只能用于`interface`。

    // 正确
    interface Foo {
      add(num:number): this;
    };

    // 报错
    type Foo = {
      add(num:number): this;
    };

上面示例中，type 命令声明的方法`add()`，返回`this`就报错了。interface 命令没有这个问题。

下面是返回`this`的实际对象的例子。

    class Calculator implements Foo {
      result = 0;
      add(num:number) {
        this.result += num;
        return this;
      }
    }

（6）type 可以扩展原始数据类型，interface 不行。

    // 正确
    type MyStr = string & {
      type: 'new'
    };

    // 报错
    interface MyStr extends string {
      type: 'new'
    }

上面示例中，type 可以扩展原始数据类型 string，interface 就不行。

（7）`interface`无法表达某些复杂类型（比如交叉类型和联合类型），但是`type`可以。

    type A = { /* ... */ };
    type B = { /* ... */ };

    type AorB = A | B;
    type AorBwithName = AorB & {
      name: string
    };

上面示例中，类型`AorB`是一个联合类型，`AorBwithName`则是为`AorB`添加一个属性。这两种运算，`interface`都没法表达。

综上所述，如果有复杂的类型运算，那么没有其他选择只能使用`type`；一般情况下，`interface`灵活性比较高，便于扩充类型或自动合并，建议优先使用。

本教程采用知识共享 署名-相同方式共享 3.0协议。

分享本文