接口的合并

一个接口可以与另一个接口合并,形成新的接口类型。新的接口拥有参与合并的所有接口的属性及方法。在 TypeScript 中,主要有 3 种接口合并方式——接口继承、交叉类型、声明合并。

接口继承

在声明新接口时,从另一个接口或多个接口继承。新接口拥有被继承接口的所有属性和方法。继承接口的语法如下。

interface 接口名称 extends 被继承接口1,被继承接口2,...,被继承接口n {
    属性名称1: 属性类型,
    属性名称2: 属性类型,
    ...
    方法名称1: 函数调用签名,
    方法名称2: 函数调用签名,
    ...
}

示例代码如下。

interface Animal {
    name: string,
    age: number,
    eat: (food: string) => void
}

interface Bird extends Animal {
    wings: string,
    fly: () => void
}

interface Eagle extends Bird {
    attack: (target: Animal) => void
}

let eagle1: Eagle = {
    age: 1,
    name: "Hedwig",
    wings: "Eagle wings",
    eat: function (food: string) { console.log(`${this.name}正在吃${food}`); },
    fly: function () { console.log("飞行中"); },
    attack: function (target: Animal) { console.log(`${this.name}正在攻击${target.name}`) }
}

以上代码先声明了一个 Animal 接口(表示动物),它拥有 nameage 属性,以及一个 eat() 方法。然后,声明了一个 Bird 接口,该接口继承自 Animal 接口,因此它也具有 Animal 接口的属性和方法。同时,Bird 接口还定义了自己的 wings 属性和 fly() 方法。最后,声明了一个 Eagle 接口,它继承自 Bird 接口。这意味着它具有 BirdAnimal 接口的所有属性与方法。同时,它还定义了自己的 attack() 方法,在代码末尾声明了一个 Eagle 类型的变量 eagle1,并把该变量指定为一个符合 Eagle 接口结构的对象。

交叉类型

通常来说,通过接口继承,你就可以实现接口功能的合并,但也可以使用 TypeScript 的交叉类型做到这一点(但推荐优先使用接口继承),交叉类型还会在后面详细介绍,这里只简要介绍如何用它来合并接口。

交叉类型使用 “&” 符号来连接多个类型。例如,以下代码先分别声明了一个 Colorful 接口和一个 Circle 接口,它们拥有各自的属性和方法。然后,声明了一个名为 ColorfulCircle 的类型别名,它的具体类型是 Colorful 接口和 Circle 接口的交叉类型,因此 ColorfulCircle 将具有两个接口的所有属性和方法。最后,声明了一个 ColorfulCircle 类型的变量 circle1,并把该变量指定为一个符合 ColorfulCircle 类型结构的对象。

interface Colorful {
    color: string
}
interface Circle {
    radius: number,
    rollling: () => void
}

type ColorfulCircle = Colorful & Circle;
let circle1: ColorfulCircle = {
    color: "red",
    radius: 5,
    rollling: function () { console.log("圆环滚动中!") }
}

声明合并

声明合并是指当声明多个同名接口时,它们将自动合并为一个接口,并同时拥有所有接口声明中的全部属性和方法。

例如,以下代码先声明了两个接口,第一个接口拥有一个 name 属性,第二个接口拥有一个 introduction() 方法,但它们都具有同样的接口名称 Person,因此它们将被合并为同一个接口。这个接口拥有所有的属性和方法。然后,定义了一个 Person 类型的变量 person1,并把该变量指定为一个符合 Person 类型结构的对象。

interface Person {
    name: string
}

interface Person {
    introduction: () => void
}

let person1: Person = {
    name: "Shank",
    introduction: function () {
        console.log(`My name is ${this.name}`);
    }
}

接口合并时的冲突

在以上 3 种接口合并方式中,如果合并时存在名称相同但类型不同的属性或方法,就可能造成冲突,引起编译错误。为保证代码的可读性和可维护性,所有的冲突都应当尽可能在编码时避免。

  1. 接口继承的属性冲突

    当使用继承接口时,如果继承接口和被继承接口拥有同名属性,但类型不匹配,在声明时就会直接引起编译错误。例如,在以下代码中,Animal 接口拥有一个 string 类型的 name 属性,WhiteMouse 接口继承自 Animal 接口,但它拥有一个 number 类型的 name 属性,因此将引起编译错误。

    interface Animal {
        name: string,
    }
    
    //编译错误:接口"WhiteMouse"错误扩展接口"Animal"。属性"name"的类型不兼容。不能将类型"number"
    //分配给类型"string"。ts(2430)
    interface WhiteMouse extends Animal {
        name: number,
    }

    注意,如果被继承接口的属性兼容继承后的接口的属性,则不会引起编译错误。例如,在以下代码中,Animal 接口拥有一个 string | number 类型的 name 属性,WhiteMouse 接口继承自 Animal 接口,它拥有一个 number 类型的 name 属性,由于被继承接口 Animalname 属性能够兼容 number 类型,因此不会引起编译错误。

    interface Animal {
        name: string | number,
    }
    
    interface WhiteMouse extends Animal {
        name: number,
    }
  2. 接口继承的方法冲突

    当使用继承接口时,如果继承接口和被继承接口拥有同名方法,但参数个数、参数类型、返回值有不匹配项,就会引起编译错误。例如,在以下代码中,Animal 接口拥有一个传入 string 类型 food 参数的 eat() 方法,Tiger 接口继承了 Animal 接口,但它传入 Animal 类型 food 参数的 eat() 方法,因此将引起编译错误。

    interface Animal {
        eat: (food: string) => void
    }
    
    //编译错误:接口"Tiger"错误扩展接口"Animal"。属性"eat"的类型不兼容。
    //不能将类型"(food: Animal) => void"分配给类型"(food: string) => void"
    //不能将类型"string"分配给类型"Animal"。ts(2430)
    interface Tiger extends Animal {
        eat: (food: Animal) => void
    }

    注意,如果继承接口的方法兼容被继承接口的方法(和属性的兼容顺序正好相反),则不会引起编译错误。例如,在以下代码中,Tiger 接口继承了 Animal 接口,但它传入 Animal | string 类型 food 参数的 eat() 方法,由于继承接口的方法兼容被继承接口的方法,因此不会引起编译错误。

    interface Animal {
        eat: (food: string) => void
    }
    
    interface Tiger extends Animal {
        eat: (food: Animal | string) => void
    }
  3. 交叉类型的属性冲突

    当使用交叉类型时,如果存在同名属性,但类型不匹配,合并后的属性的类型是 never(表示不存在符合的值)。通常在声明时这不会引起编译错误,但在赋值时由于没有匹配 never 类型的值,因此将引起编译错误。例如,在以下代码中,接口A 的 name 属性为 string 类型,接口B 的 name 属性为 number 类型,将它们交叉为 C 类型。由于 name 属性不同,交叉后成为 never 类型,因此将无法为其赋值。

    interface A {
        name: string
    }
    
    interface B {
        name: number
    }
    
    type C = A & B;
    
    //编译错误:不能将类型"string"分配给类型"never"。ts(2322)
    let object1: C = { name: "a" }
    //编译错误:不能将类型"string"分配给类型"never"。ts(2322)
    let object1: C = { name: 1 }
  4. 交叉类型的方法冲突

    当使用交叉类型时,如果存在同名方法,但参数个数、参数类型或返回值不匹配,那么合并后的方法也将成为一种奇怪的类型。它通常不会在声明时引起编译错误,但在赋值时会失去预期的编译检查效果。示例代码如下。

    interface A {
        sum: (a: number, b: number) => number
    }
    
    interface B {
        sum: (a: string, b: string, c: string) => string
    }
    
    type C = A & B;
    
    let object1: C = {
        //类型为(property) sum: ((a: number, b: number) => number) & ((a: string,  b: string,
        //c: string) => string)
        //编译检查通过
        sum: function(): any {
            return "";
        }
    }

    应尽可能避免出现这种冲突。