likes
comments
collection
share

TS 中的接口

作者站长头像
站长
· 阅读数 3

刚接触 typescript 的同学可能会对接口这个概念感到陌生,毕竟 js 里可没有这么个玩意儿。本篇文章就来介绍一下,到底什么是接口?它又能起到什么作用?

基础用法

接口是对象的状态和行为的抽象,使用接口来对一个对象的属性和方法的类型进行声明。比如,我们需要定义个歌手对象,其有以下 4 种属性:

  1. id:字符串类型,只读的,必需的;
  2. name:字符串类型,必需的;
  3. age:数字类型,必需的;
  4. gender:字符串类型,可选的。

那么接口就可以这么写: 使用关键字 interface 定义接口,写法同定义 class 类相似,接口名后不用跟括号,我习惯接口名以 I 开头,{} 里的内容不需要用逗号分割:

interface ISinger {
  readonly id: string // readonly 写在属性名前,代表只读属性,赋值后不能修改
  name: string
  age: number
  gender?: string // 属性名后跟个问号代表是可选属性
  sing(): void // 定义方法
  run: () => void // 也可以写成这样定义方法
}

在定义一个对象的时候,类型就可以是接口 ISinger

const Jay: ISinger = {
  id: 'z1234',
  name: 'Jay',
  age: 22,
  sing() { },
  run() { }
}

除了可选属性,其它属性的必须要进行定义,不能多,也不能少。但是,如果是下面这种写法,则又允许将拥有 IPerson 接口中不存在的属性的对象 temObj 赋值给 Jay

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 方法,就会报错:

TS 中的接口

但如果我们把字面量对象先赋值给 temp ,在把 temp 赋值给 singer,就不会报错:

type singerType = {
  name: string
  age: number
}
const temp = {
  name: 'Jay',
  age: 18,
  sing() { }
}

const singer: singerType = temp

这是因为 ts 在做类型检测时,会默认把 temp 中多出来的 sing 属性去除,所以,如果我们之后调用 sing 方法,类型检测也会报错:

TS 中的接口

类对接口的实现

implements

类除了可以继承类,也可以实现(implements)接口:

interface ISinger {
  sing(): void // 该方法没有任何的实现
}

class Singer implements ISinger {
  sing() {
    console.log('作为一名歌手一年出张专辑不过分吧~')
  }
}

const Jay = new Singer()
Jay.sing()

接口 ISinger 定义了 sing 方法,类 Singer 实现了 ISinger,则 Singer 类中也必须定义 sing 方法。

一个类可以实现多个接口

一个类只可以继承自一个类,但可以实现多个接口,接口之间用 , 分割。每个接口中的内容都要真正实现:

interface ISinger {
  sing(): void
}
interface IFatPersong {
  eat(): void
}

class Singer implements ISinger, IFatPersong {
  sing() {
    console.log('作为一名歌手一年出张专辑不过分吧~')
  }
  eat() {
    console.log('就知道喝奶茶')
  }
}

接口继承接口

和类一样,接口也可以相互继承,一个接口可以继承多个接口。上面一个类可以实现多个接口例子也可以用下面这种方式:

interface IFatSinger extends ISinger, IFatPersong {}

// 直接实现上面这个类
class Singer implements IFatSinger {
  sing() {}
  eat() {}
}

注意:接口之间是继承(extends)关系,类和接口之间是实现(implements)关系。

和 type 对比

  • 写法的区别,type 有用到 =,是赋值式的写法;而 interface 的定义没有用到 =,是声明式的写法:
type PersonType = {
  name: string
  age?: number
}

interface IPerson {
  name: string
  age?: number
}
  • type 可以定义的类型范围比 interface 大,一般定义函数或者联合类型时,使用 type,而 interface 用于定义对象,并且官方文档有下面这么一句话 :

在大多数情况下,你可以根据个人喜好进行选择,TypeScript 会告诉你它是否需要其他类型的声明。如果您想要启发式方法,可以使用 interface 直到你需要使用 type 中的功能。

  • interface 可以重复定义,而 type 不可以。多次定义的 ISinger 接口的属性会合并,所以 Singer 需要同时有 singeat 方法:
interface ISinger {
  sing(): void
}
interface ISinger {
  eat(): void
}

class Singer implements ISinger {
  sing() { }
  eat() { }
}
  • 接口可以被类实现,并且支持继承,而 type 则不行。

索引签名(Index Signatures)

当我们想定义一个属性名均为数字类型的对象 obj1,可以设置接口的 key 的类型,当然这里第 2 行的 key 只是个形参,是自定义的,类型可以是stringnumbersymbol

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 的值的子类型:

interface IObj {
  [key1: string]: string | number
  [key2: number]: string
}

如果像下面这样,就会报错:

TS 中的接口

因为在 js 中,使用数字进行索引时,实际上会在索引到对象之前将数字转换成字符串,即 arr[1]arr['1'] 是一样的。

同理,如果定义了索引类型为 string 的索引签名,又定义了其它属性,那么其它属性的值的类型,也必须是索引类型为 string 的值的类型的子类型:

interface IObj {
  [key: string]: string | number
  name: string
}

TS 中的接口 TS 中的接口