接口的合并

一个接口可以与另一个接口合并,形成新的接口类型。新的接口拥有参与合并的所有接口的属性及方法。在 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 接口(表示动物),它拥有 name 和 age 属性,以及一个 eat() 方法。然后,声明了一个 Bird 接口,该接口继承自 Animal 接口,因此它也具有 Animal 接口的属性和方法。同时,Bird 接口还定义了自己的 wings 属性和 fly() 方法。最后,声明了一个 Eagle 接口,它继承自 Bird 接口。这意味着它具有 Bird 和 Animal 接口的所有属性与方法。同时,它还定义了自己的 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 属性,由于被继承接口 Animal 的 name 属性能够兼容 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 "";
        }
    }

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