三、接口使用
1. 定义函数类型
在前面函数类型篇我们说了,可以使用接口来定义函数类型:
interface AddFunc { (num1: number, num2: number): number; } 复制代码
这里定义了一个AddFunc结构,这个结构要求实现这个结构的值,必须包含一个和结构里定义的函数一样参数、一样返回值的方法,或者这个值就是符合这个函数要求的函数。把花括号里包着的内容称为调用签名,它由带有参数类型的参数列表和返回值类型组成:
const add: AddFunc = (n1, n2) => n1 + n2; const join: AddFunc = (n1, n2) => `${n1} ${n2}`; // 不能将类型'string'分配给类型'number' add("a", 2); // 类型'string'的参数不能赋给类型'number'的参数 复制代码
上面定义的add函数接收两个数值类型的参数,返回的结果也是数值类型,所以没有问题。而join函数参数类型没错,但是返回的是字符串,所以会报错。而当调用add函数时,传入的参数如果和接口定义的类型不一致,也会报错。在实际定义函数的时候,名字是无需和接口中参数名相同的,只需要位置对应即可。
实际上,很少使用接口类型来定义函数类型,更多使用内联类型或类型别名配合箭头函数语法来定义函数类型:
type AddFunc = (num1: number, num2: number) => number; 复制代码
这里给箭头函数类型指定了一个别名 AddFunc,在其他地方就可以直接复用 AddFunc,而不用重新声明新的箭头函数类型定义。
2. 定义索引类型
在实际工作中,使用接口类型较多的地方是对象,比如 React 组件的 Props & State等,这些对象有一个共性,即所有的属性名、方法名都是确定的。
实际上,经常会把对象当 Map 映射使用,比如下边代码中定义了索引是任意数字的对象 role1 和索引是任意字符串的对象 role2。
const role1 = { 0: "super_admin", 1: "admin" }; const role2 = { s: "super_admin", a: "admin" }; 复制代码
这时需要使用索引签名来定义上边提到的对象映射结构,并通过 “[索引名: 类型]”的格式约束索引的类型。索引名称的类型分为 string 和 number 两种,通过如下定义的 RoleDic 和 RoleDic1 两个接口,可以用来描述索引是任意数字或任意字符串的对象:
interface RoleDic { [id: number]: string; } interface RoleDic1 { [id: string]: string; } const role1: RoleDic = { 0: "super_admin", 1: "admin" }; const role2: RoleDic = { s: "super_admin", // error 不能将类型"{ s: string; a: string; }"分配给类型"RoleDic"。 a: "admin" }; const role3: RoleDic = ["super_admin", "admin"]; 复制代码
需要注意,当使用数字作为对象索引时,它的类型既可以与数字兼容,也可以与字符串兼容,这与 JavaScript 的行为一致。因此,使用 0 或 '0' 索引对象时,这两者等价。
上面的 role3 定义了一个数组,索引为数值类型,值为字符串类型。我们还可以给索引设置readonly,从而防止索引返回值被修改:
interface RoleDic { readonly [id: number]: string; } const role: RoleDic = { 0: "super_admin" }; role[0] = "admin"; // error 类型"RoleDic"中的索引签名仅允许读取 复制代码
注意,可以设置索引类型为 number。但是这样如果将属性名设置为字符串类型,则会报错;但是如果设置索引类型为字符串类型,那么即便属性名设置的是数值类型,也没问题。因为 JS 在访问属性值时,如果属性名是数值类型,会先将数值类型转为字符串,然后再去访问:
const obj = { 123: "a", "123": "b" // 报错:标识符“"123"”重复。 }; console.log(obj); // { '123': 'b' } 复制代码
如果数值类型的属性名不会转为字符串类型,那么这里数值123和字符串123是不同的两个值,则最后对象obj应该同时有这两个属性;但是实际打印出来的obj只有一个属性,属性名为字符串"123",值为"b",说明数值类型属性名123被覆盖掉了,就是因为它被转为了字符串类型属性名"123";又因为一个对象中多个相同属性名的属性,定义在后面的会覆盖前面的,所以结果就是obj只保留了后面定义的属性值。
四、高级用法
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。所以,接口类型是可重复定义且属性会叠加的,而类型别名是不可重复定义的。