刚接触 typescript 的同学可能会对接口这个概念感到陌生,毕竟 js 里可没有这么个玩意儿。本篇文章就来介绍一下,到底什么是接口?它又能起到什么作用?
基础用法
接口是对象的状态和行为的抽象,使用接口来对一个对象的属性和方法的类型进行声明。比如,我们需要定义个歌手对象,其有以下 4 种属性:
- id:字符串类型,只读的,必需的;
- name:字符串类型,必需的;
- age:数字类型,必需的;
- gender:字符串类型,可选的。
那么接口就可以这么写: 使用关键字 interface
定义接口,写法同定义 class
类相似,接口名后不用跟括号,我习惯接口名以 I
开头,{}
里的内容不需要用逗号分割:
typescript
interface ISinger {
readonly id: string // readonly 写在属性名前,代表只读属性,赋值后不能修改
name: string
age: number
gender?: string // 属性名后跟个问号代表是可选属性
sing(): void // 定义方法
run: () => void // 也可以写成这样定义方法
}
在定义一个对象的时候,类型就可以是接口 ISinger
:
typescript
const Jay: ISinger = {
id: 'z1234',
name: 'Jay',
age: 22,
sing() { },
run() { }
}
除了可选属性,其它属性的必须要进行定义,不能多,也不能少。但是,如果是下面这种写法,则又允许将拥有 IPerson
接口中不存在的属性的对象 temObj
赋值给 Jay
:
typescript
interface IPerson {
name: string
age: number
}
const temObj = {
name: 'Jay',
age: 22,
height: 12 // IPerson 中不存在的属性
}
const Jay: IPerson = temObj
这种现象的原因,与下面介绍的 freshness 检测规则有关:
Freshness
在对字面量进行赋值时,ts 的类型检测有个叫做 freshness(字面意思为"新鲜") 的规则。比如下例中 singer
类型为 singerType
,而直接赋值的字面量对象里多了个 sing
方法,就会报错:
但如果我们把字面量对象先赋值给 temp
,在把 temp
赋值给 singer
,就不会报错:
typescript
type singerType = {
name: string
age: number
}
const temp = {
name: 'Jay',
age: 18,
sing() { }
}
const singer: singerType = temp
这是因为 ts 在做类型检测时,会默认把 temp
中多出来的 sing
属性去除,所以,如果我们之后调用 sing
方法,类型检测也会报错:
类对接口的实现
implements
类除了可以继承类,也可以实现(implements
)接口:
typescript
interface ISinger {
sing(): void // 该方法没有任何的实现
}
class Singer implements ISinger {
sing() {
console.log('作为一名歌手一年出张专辑不过分吧~')
}
}
const Jay = new Singer()
Jay.sing()
接口 ISinger
定义了 sing
方法,类 Singer
实现了 ISinger
,则 Singer
类中也必须定义 sing
方法。
一个类可以实现多个接口
一个类只可以继承自一个类,但可以实现多个接口,接口之间用 ,
分割。每个接口中的内容都要真正实现:
typescript
interface ISinger {
sing(): void
}
interface IFatPersong {
eat(): void
}
class Singer implements ISinger, IFatPersong {
sing() {
console.log('作为一名歌手一年出张专辑不过分吧~')
}
eat() {
console.log('就知道喝奶茶')
}
}
接口继承接口
和类一样,接口也可以相互继承,一个接口可以继承多个接口。上面一个类可以实现多个接口例子也可以用下面这种方式:
typescript
interface IFatSinger extends ISinger, IFatPersong {}
// 直接实现上面这个类
class Singer implements IFatSinger {
sing() {}
eat() {}
}
注意:接口之间是继承(extends
)关系,类和接口之间是实现(implements
)关系。
和 type 对比
在 《TS 中的类型》中介绍了定义类型别名的 type
, 它和 interface
都可以定义对象的类型,它们有什么区别呢?
- 写法的区别,
type
有用到=
,是赋值式的写法;而interface
的定义没有用到=
,是声明式的写法:
typescript
type PersonType = {
name: string
age?: number
}
interface IPerson {
name: string
age?: number
}
type
可以定义的类型范围比interface
大,一般定义函数或者联合类型时,使用type
,而interface
用于定义对象,并且官方文档有下面这么一句话 :
在大多数情况下,你可以根据个人喜好进行选择,TypeScript 会告诉你它是否需要其他类型的声明。如果您想要启发式方法,可以使用 interface 直到你需要使用 type 中的功能。
interface
可以重复定义,而type
不可以。多次定义的ISinger
接口的属性会合并,所以Singer
需要同时有sing
和eat
方法:
typescript
interface ISinger {
sing(): void
}
interface ISinger {
eat(): void
}
class Singer implements ISinger {
sing() { }
eat() { }
}
- 接口可以被类实现,并且支持继承,而 type 则不行。
索引签名(Index Signatures)
当我们想定义一个属性名均为数字类型的对象 obj1
,可以设置接口的 key 的类型,当然这里第 2 行的 key
只是个形参,是自定义的,类型可以是string
、number
或 symbol
:
typescript
interface IObj1 {
[key: number]: string
}
const obj1: IObj1 = {
0: 'Jay',
1: 'Join',
2: 'Eson'
}
interface IObj2 {
[key: symbol]: string
}
const sym: symbol = Symbol()
const obj2: IObj2 = {
[sym]: 'Jay',
}
当 obj1
的类型被注释为 IObj1
后,obj1
的属性名就只能是数字了。
另外还有一个细节,索引类型为 number
的值的类型,必须是索引类型为 string
的值的子类型:
typescript
interface IObj {
[key1: string]: string | number
[key2: number]: string
}
如果像下面这样,就会报错:
因为在 js 中,使用数字进行索引时,实际上会在索引到对象之前将数字转换成字符串,即 arr[1]
和 arr['1']
是一样的。
同理,如果定义了索引类型为 string
的索引签名,又定义了其它属性,那么其它属性的值的类型,也必须是索引类型为 string
的值的类型的子类型:
typescript
interface IObj {
[key: string]: string | number
name: string
}