TypeScript 的 interface 接口
TypeScript 的 interface 接口
网道(WangDoc.com),互联网文档计划
简介 #
interface 是对象的模板,可以看作是一种类型约定,中文译为“接口”。使用了某个模板的对象,就拥有了指定的类型结构。
1 | interface Person { |
上面示例中,定义了一个接口Person
,它指定一个对象模板,拥有三个属性firstName
、lastName
和age
。任何实现这个接口的对象,都必须部署这三个属性,并且必须符合规定的类型。
实现该接口很简单,只要指定它作为对象的类型即可。
1 | const p:Person = { |
上面示例中,变量p
的类型就是接口Person
,所以必须符合Person
指定的结构。
方括号运算符可以取出 interface 某个属性的类型。
1 | interface Foo { |
上面示例中,Foo['a']
返回属性a
的类型,所以类型A
就是string
。
interface 可以表示对象的各种语法,它的成员有5种形式。
- 对象属性
- 对象的属性索引
- 对象方法
- 函数
- 构造函数
(1)对象属性
1 | interface Point { |
上面示例中,x
和y
都是对象的属性,分别使用冒号指定每个属性的类型。
属性之间使用分号或逗号分隔,最后一个属性结尾的分号或逗号可以省略。
如果属性是可选的,就在属性名后面加一个问号。
1 | interface Foo { |
如果属性是只读的,需要加上readonly
修饰符。
1 | interface A { |
(2)对象的属性索引
1 | interface A { |
上面示例中,[prop: string]
就是属性的字符串索引,表示属性名只要是字符串,都符合类型要求。
属性索引共有string
、number
和symbol
三种类型。
一个接口中,最多只能定义一个字符串索引。字符串索引会约束该类型中所有名字为字符串的属性。
1 | interface MyObj { |
上面示例中,属性索引指定所有名称为字符串的属性,它们的属性值必须是数值(number
)。属性a
的值为布尔值就报错了。
属性的数值索引,其实是指定数组的类型。
1 | interface A { |
上面示例中,[prop: number]
表示属性名的类型是数值,所以可以用数组对变量obj
赋值。
同样的,一个接口中最多只能定义一个数值索引。数值索引会约束所有名称为数值的属性。
如果一个 interface 同时定义了字符串索引和数值索引,那么数值索引必须服从于字符串索引。因为在 JavaScript 中,数值属性名最终是自动转换成字符串属性名。
1 | interface A { |
上面示例中,数值索引的属性值类型与字符串索引不一致,就会报错。数值索引必须兼容字符串索引的类型声明。
(3)对象的方法
对象的方法共有三种写法。
1 | // 写法一 |
属性名可以采用表达式,所以下面的写法也是可以的。
1 | const f = 'f'; |
类型方法可以重载。
1 | interface A { |
interface 里面的函数重载,不需要给出实现。但是,由于对象内部定义方法时,无法使用函数重载的语法,所以需要额外在对象外部给出函数方法的实现。
1 | interface A { |
上面示例中,接口A
的方法f()
有函数重载,需要额外定义一个函数MyFunc()
实现这个重载,然后部署接口A
的对象a
的属性f
等于函数MyFunc()
就可以了。
(4)函数
interface 也可以用来声明独立的函数。
1 | interface Add { |
上面示例中,接口Add
声明了一个函数类型。
(5)构造函数
interface 内部可以使用new
关键字,表示构造函数。
1 | interface ErrorConstructor { |
上面示例中,接口ErrorConstructor
内部有new
命令,表示它是一个构造函数。
TypeScript 里面,构造函数特指具有constructor
属性的类,详见《Class》一章。
interface 的继承 #
interface 可以继承其他类型,主要有下面几种情况。
interface 继承 interface #
interface 可以使用extends
关键字,继承其他 interface。
1 | interface Shape { |
上面示例中,Circle
继承了Shape
,所以Circle
其实有两个属性name
和radius
。这时,Circle
是子接口,Shape
是父接口。
extends
关键字会从继承的接口里面拷贝属性类型,这样就不必书写重复的属性。
interface 允许多重继承。
1 | interface Style { |
上面示例中,Circle
同时继承了Style
和Shape
,所以拥有三个属性color
、name
和radius
。
多重接口继承,实际上相当于多个父接口的合并。
如果子接口与父接口存在同名属性,那么子接口的属性会覆盖父接口的属性。注意,子接口与父接口的同名属性必须是类型兼容的,不能有冲突,否则会报错。
1 | interface Foo { |
上面示例中,Bar
继承了Foo
,但是两者的同名属性id
的类型不兼容,导致报错。
多重继承时,如果多个父接口存在同名属性,那么这些同名属性不能有类型冲突,否则会报错。
1 | interface Foo { |
上面示例中,Baz
同时继承了Foo
和Bar
,但是后两者的同名属性id
有类型冲突,导致报错。
interface 继承 type #
interface 可以继承type
命令定义的对象类型。
1 | type Country = { |
上面示例中,CountryWithPop
继承了type
命令定义的Country
对象,并且新增了一个population
属性。
注意,如果type
命令定义的类型不是对象,interface 就无法继承。
interface 继承 class #
interface 还可以继承 class,即继承该类的所有成员。关于 class 的详细解释,参见下一章。
1 | class A { |
上面示例中,B
继承了A
,因此B
就具有属性x
、y()
和z
。
实现B
接口的对象就需要实现这些属性。
1 | const b:B = { |
上面示例中,对象b
就实现了接口B
,而接口B
又继承了类A
。
某些类拥有私有成员和保护成员,interface 可以继承这样的类,但是意义不大。
1 | class A { |
上面示例中,A
有私有成员和保护成员,B
继承了A
,但无法用于对象,因为对象不能实现这些成员。这导致B
只能用于其他 class,而这时其他 class 与A
之间不构成父类和子类的关系,使得x
与y
无法部署。
接口合并 #
多个同名接口会合并成一个接口。
1 | interface Box { |
上面示例中,两个Box
接口会合并成一个接口,同时有height
、width
和length
三个属性。
这样的设计主要是为了兼容 JavaScript 的行为。JavaScript 开发者常常对全局对象或者外部库,添加自己的属性和方法。那么,只要使用 interface 给出这些自定义属性和方法的类型,就能自动跟原始的 interface 合并,使得扩展外部类型非常方便。
举例来说,Web 网页开发经常会对window
对象和document
对象添加自定义属性,但是 TypeScript 会报错,因为原始定义没有这些属性。解决方法就是把自定义属性写成 interface,合并进原始定义。
1 | interface Document { |
上面示例中,接口Document
增加了一个自定义属性foo
,从而就可以在document
对象上使用自定义属性。
同名接口合并时,同一个属性如果有多个类型声明,彼此不能有类型冲突。
1 | interface A { |
上面示例中,接口A
的属性a
有两个类型声明,彼此是冲突的,导致报错。
同名接口合并时,如果同名方法有不同的类型声明,那么会发生函数重载。而且,后面的定义比前面的定义具有更高的优先级。
1 | interface Cloner { |
上面示例中,clone()
方法有不同的类型声明,会发生函数重载。这时,越靠后的定义,优先级越高,排在函数重载的越前面。比如,clone(animal: Animal)
是最先出现的类型声明,就排在函数重载的最后,属于clone()
函数最后匹配的类型。
这个规则有一个例外。同名方法之中,如果有一个参数是字面量类型,字面量类型有更高的优先级。
1 | interface A { |
上面示例中,f()
方法有一个类型声明的参数x
是字面量类型,这个类型声明的优先级最高,会排在函数重载的最前面。
一个实际的例子是 Document 对象的createElement()
方法,它会根据参数的不同,而生成不同的 HTML 节点对象。
1 | interface Document { |
上面示例中,createElement()
方法的函数重载,参数为字面量的类型声明会排到最前面,返回具体的 HTML 节点对象。类型越不具体的参数,排在越后面,返回通用的 HTML 节点对象。
如果两个 interface 组成的联合类型存在同名属性,那么该属性的类型也是联合类型。
1 | interface Circle { |
上面示例中,接口Circle
和Rectangle
组成一个联合类型Circle | Rectangle
。因此,这个联合类型的同名属性area
,也是一个联合类型。本例中的declare
命令表示变量s
的具体定义,由其他脚本文件给出,详见《declare 命令》一章。
interface 与 type 的异同 #
interface
命令与type
命令作用类似,都可以表示对象类型。
很多对象类型既可以用 interface 表示,也可以用 type 表示。而且,两者往往可以换用,几乎所有的 interface 命令都可以改写为 type 命令。
它们的相似之处,首先表现在都能为对象类型起名。
1 | type Country = { |
上面示例是type
命令和interface
命令,分别定义同一个类型。
class
命令也有类似作用,通过定义一个类,同时定义一个对象类型。但是,它会创造一个值,编译后依然存在。如果只是单纯想要一个类型,应该使用type
或interface
。
interface 与 type 的区别有下面几点。
(1)type
能够表示非对象类型,而interface
只能表示对象类型(包括数组、函数等)。
(2)interface
可以继承其他类型,type
不支持继承。
继承的主要作用是添加属性,type
定义的对象类型如果想要添加属性,只能使用&
运算符,重新定义一个类型。
1 | type Animal = { |
上面示例中,类型Bear
在Animal
的基础上添加了一个属性honey
。
上例的&
运算符,表示同时具备两个类型的特征,因此可以起到两个对象类型合并的作用。
作为比较,interface
添加属性,采用的是继承的写法。
1 | interface Animal { |
继承时,type 和 interface 是可以换用的。interface 可以继承 type。
1 | type Foo = { x: number; }; |
type 也可以继承 interface。
1 | interface Foo { |
(3)同名interface
会自动合并,同名type
则会报错。也就是说,TypeScript 不允许使用type
多次定义同一个类型。
1 | type A = { foo:number }; // 报错 |
上面示例中,type
两次定义了类型A
,导致两行都会报错。
作为比较,interface
则会自动合并。
1 | interface A { foo:number }; |
上面示例中,interface
把类型A
的两个定义合并在一起。
这表明,interface 是开放的,可以添加属性,type 是封闭的,不能添加属性,只能定义新的 type。
(4)interface
不能包含属性映射(mapping),type
可以,详见《映射》一章。
1 | interface Point { |
(5)this
关键字只能用于interface
。
1 | // 正确 |
上面示例中,type 命令声明的方法add()
,返回this
就报错了。interface 命令没有这个问题。
下面是返回this
的实际对象的例子。
1 | class Calculator implements Foo { |
(6)type 可以扩展原始数据类型,interface 不行。
1 | // 正确 |
上面示例中,type 可以扩展原始数据类型 string,interface 就不行。
(7)interface
无法表达某些复杂类型(比如交叉类型和联合类型),但是type
可以。
1 | type A = { /* ... */ }; |
上面示例中,类型AorB
是一个联合类型,AorBwithName
则是为AorB
添加一个属性。这两种运算,interface
都没法表达。
综上所述,如果有复杂的类型运算,那么没有其他选择只能使用type
;一般情况下,interface
灵活性比较高,便于扩充类型或自动合并,建议优先使用。
本文转自 https://wangdoc.com/typescript/interface,如有侵权,请联系删除。