接口的合并
一个接口可以与另一个接口合并,形成新的接口类型。新的接口拥有参与合并的所有接口的属性及方法。在 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 种接口合并方式中,如果合并时存在名称相同但类型不同的属性或方法,就可能造成冲突,引起编译错误。为保证代码的可读性和可维护性,所有的冲突都应当尽可能在编码时避免。
-
接口继承的属性冲突
当使用继承接口时,如果继承接口和被继承接口拥有同名属性,但类型不匹配,在声明时就会直接引起编译错误。例如,在以下代码中,
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, }
-
接口继承的方法冲突
当使用继承接口时,如果继承接口和被继承接口拥有同名方法,但参数个数、参数类型、返回值有不匹配项,就会引起编译错误。例如,在以下代码中,
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 }
-
交叉类型的属性冲突
当使用交叉类型时,如果存在同名属性,但类型不匹配,合并后的属性的类型是
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 }
-
交叉类型的方法冲突
当使用交叉类型时,如果存在同名方法,但参数个数、参数类型或返回值不匹配,那么合并后的方法也将成为一种奇怪的类型。它通常不会在声明时引起编译错误,但在赋值时会失去预期的编译检查效果。示例代码如下。
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 ""; } }
应尽可能避免出现这种冲突。