本文已参与「新人创作礼」活动,一起开启掘金创作之路。
四、高级用法
1. 继承接口
在 TypeScript 中,接口是可以继承,这和ES6中的类一样,这提高了接口的可复用性。来看一个场景:定义一个Vegetables接口,它会对color属性进行限制。再定义两个接口Tomato和Carrot,这两个类都需要对color进行限制,而各自又有各自独有的属性限制,可以这样定义:
interface Vegetables {
color: string;
}
interface Tomato {
color: string;
radius: number;
}
interface Carrot {
color: string;
length: number;
}
三个接口中都有对color的定义,但是这样写很繁琐,可以用继承来改写:
interface Vegetables {
color: string;
}
interface Tomato extends Vegetables {
radius: number;
}
interface Carrot extends Vegetables {
length: number;
}
const tomato: Tomato = {
radius: 1.2 // error Property 'color' is missing in type '{ radius: number; }'
};
const carrot: Carrot = {
color: "orange",
length: 20
};
上面定义的 tomato 变量因为缺少了从Vegetables接口继承来的 color 属性,所以报错了。
一个接口可以被多个接口继承,同样,一个接口也可以继承多个接口,多个接口用逗号隔开。比如再定义一个Food接口,Tomato 也可以继承 Food:
interface Vegetables {
color: string;
}
interface Food {
type: string;
}
interface Tomato extends Food, Vegetables {
radius: number;
}
const tomato: Tomato = {
type: "vegetables",
color: "red",
radius: 1
};
如果想要覆盖掉继承的属性,那就只能使用兼容的类型进行覆盖:
interface Tomato extends Vegetables {
color: number;
}
这里我们将color属性进行了覆盖,并将其类型设置为了number类型,这时就会报错,因为Tomato 和 Vegetables 中的name属性是不兼容的。
2. 混合类型接口
在 JavaScript 中,函数是对象类型。对象可以有属性,所以有时一个对象既是一个函数,也包含一些属性。比如要实现一个计数器函数,比较直接的做法是定义一个函数和一个全局变量:
let count = 0;
const counter = () => count++;
但是这种方法需要在函数外面定义一个变量,更优一点的方法是使用闭包:
const counter = (() => {
let count = 0;
return () => {
return count++;
};
})();
console.log(counter()); // 1
console.log(counter()); // 2
TypeScript 支持直接给函数添加属性,这在 JavaScript 中是支持的:
let counter = () => {
return counter.count++;
};
counter.count = 0;
console.log(counter()); // 1
console.log(counter()); // 2
这里把一个函数赋值给countUp,又给它绑定了一个属性count,计数保存在这个 count 属性中。
可以使用混合类型接口来指定上面例子中 counter 的类型:
interface Counter {
(): void;
count: number;
}
const getCounter = (): Counter => {
const c = () => {
c.count++;
};
c.count = 0;
return c;
};
const counter: Counter = getCounter();
counter();
console.log(counter.count); // 1
counter();
console.log(counter.count); // 2
这里定义了一个Counter接口,这个结构必须包含一个函数,函数的要求是无参数,返回值为void,即无返回值。而且这个结构还必须包含一个名为count、值的类型为number类型的属性。最后,通过getCounter函数得到这个计数器。这里 getCounter 的类型为Counter,它是一个函数,无返回值,即返回值类型为void,它还包含一个属性count,属性返回值类型为number。
五、类型别名
类型别名并不属于接口类型的内容,但是它和接口功能类似,所以这里放在一起来说。
1. 基本使用
接口类型的作用就是将内联类型抽离出来,从而实现类型复用。其实,还可以使用类型别名接收抽离出来的内联类型实现复用。可以通过如下所示“type 别名名字 = 类型定义”的格式来定义类型别名,比如上面定义的计数器方法的类型:
type Counter = {
(): void;
count: number;
}
这样写看起来就像是在JavaScript中定义变量,只不过是把var、let、const换成了type。实际上,类型别名可以在接口类型无法覆盖的场景中使用,比如联合类型、交叉类型等:
// 联合类型
type Name = number | string;
// 交叉类型
type Vegetables = {color: string, radius: number} & {color: string, length: number}
这里定义了一个 Vegetables 类型别名,表示两个匿名接口类型交叉出的类型。
需要注意:类型别名只是给类型取了一个别名,并不是创建了一个新的类型。
2. 与接口区别
通过上面的介绍,可以发现多数情况下是可以使用类型别名来替代的,那这是否说明这两者是等价的呢?答案肯定是否定的,不然也不会出这两个概念。在某些特定的场景下,这两者还是存在很大区别。比如,重复定义的接口类型,它的属性会叠加,这个特性使得我们可以很方便地对全局变量、第三方库的类型做扩展:
interface Vegetables {
color: string;
}
interface Vegetables {
radius: number;
}
interface Vegetables {
length: number;
}
let vegetables: Vegetables = {
color: "red",
radius: 2,
length: 10
}
这里我们定义了三次 Vegetables 接口,此时可以赋值给变一个包含color、radius、length的对象,并且不会报错。
如果重复定义类型别名:
type Vegetables = {
color: string;
}
type Vegetables = {
radius: number;
}
type Vegetables = {
length: number;
}
let vegetables: Vegetables = {
color: "red",
radius: 2,
length: 10
}
上述代码就会报错:'Vegetables' is already defined。所以,接口类型是可重复定义且属性会叠加的,而类型别名是不可重复定义的。