聊聊TypeScript中的Interface

2,311 阅读6分钟

前言

大概半年之前学习了一下 TypeScript,那时候对其中的interfacetype就产生了疑问,它们具体有什么区别?比较火的一个讲解是这篇文章:Typescript 中的 interface 和 type 到底有什么区别,如果你现在去百度搜,到现在很多搜索结果都是这篇文章的内容,或者结论。这篇文章很不错,大家可以看看,但是有个点,我读完之后,可能仍然不清楚什么场景下应该用interface或者是type。🤔

现在最近在学 Golang,Golang 的 interface 让我对 TypeScript 中的interface有了一些想法,个人想法不一定对,大家理性讨论~

明朝的剑?

可能大家会想,这不是 Golang 嘛,和 TypeScript 完全不是同一个语言啊。

我是这么认为的:不管是什么样的编程语言,遵循的编程思维应该是一致的,虽然可能会在语法使用上以及解决的问题类型上会有所差异,但这些差异性应该不会太大。

比如说在 C 语言中的数组类型,它单纯就是一个有序的元素序列,虽然在 JS 上加上了队列加栈的用法,导致他们有所不同。但是你可以发现他们的解决问题的出发点是一致的:都是用来存放一个有序元素列表。

同一个名词的东西 ,我不太相信在不同的编程语言中,还有着巨大差别的规范。试想下:我在 C 语言用 array 定于数组,跑到 JS、TS 中用 array 想定义数组时,JS、TS 却告诉我你定义的是一个 Map。很不合理不是么?

Go 中的interface

本小节用的代码都为 Golang,便于说明问题,如果有想跑示例的代码,可以去这里尝试下在线 Go 编程

定义

我先简单介绍一下 Go 是怎么定义interface的:

interface 是 golang 最重要的特性之一,Interface 类型可以定义一组方法,但是这些不需要实现。请注意:此处限定是一组方法,既然是方法,就不能是变量;而且是一组,表明可以有多个方法。

如何理解?

  1. interface 是一种类型
  2. interface 只关心方法

为什么会有 interface?

这里先说 2 个 Go 的背景:

  1. Go 也是有type的,也可以用于做interface类似的事情。
  2. Go 函数接受参数时,不能像 TS 那样使用 function(arg: string | number)来接受形参

基于背景 2,我们来思考一种场景,假如我们现在要出门旅行了,选择了一种交通工具出发,交通工具有汽车、飞机...,每种出发方式不同,基于这个场景,我们来写写代码。

import "fmt"
type family struct{// 这里struct 可以理解为 JS中的对象// 我定义了一个家庭出游的对象 father string mother string son string
way string goTravel func(string)
}
func travel(f *family) { f.goTravel(f.way) // 将会输出 通过汽车去旅游}
func main() { f := new(family) f.way = "汽车" f.goTravel = func(way string) { fmt.Println("通过"+way+"去旅游") } travel(f)}

乍一看,好像没啥问题,完全不需要interface 不是嘛?那么假如,现在不仅人要旅游,小动物们也要旅游,根据上述说的背景 2:Go的形参必须明确一种类型,我们该怎么解决?

type Bearfamily struct{// 假设现在出游的家庭新增了熊大一家  way string	goTravel func(string)}
func travel(f *family) { // 供人类一家出游的方式 f.goTravel(f.way)}
func travelByBear(f *Bearfamily) { // 供熊大一家出游的方式 f.goTravel(f.way)}...

的确,我们能通过新增函数、以及在不同的类中重复定义函数声明解决问题,但试想一下,如果出游家庭种类多了呢?比如说兔子一家,小鸟一家...

我抽象下这个问题,现有n个方法在多个集合上声明,以后每新增一个集合,我们就得重复声明这n个方法。因此我们不能使用 type 解决这类问题,所以Go需要有interface,有兴趣的话可以简单看看 Go 中的用法,没兴趣的话我们可以进入下一节。

type goTravel interface{// 定义了接口 有个描述如何去旅游的方法	goTravel()}type family struct{	way string}type bearfamily struct{	way string}
func (b bearfamily) goTravel() { fmt.Println("熊大通过"+b.way+"去旅游")}
func (f family) goTravel() { fmt.Println("人类通过"+f.way+"去旅游")}
func travel(t goTravel) { t.goTravel() // 将会输出 通过汽车去旅游}
func main() { f := new(family) f.way = "汽车" travel(f) b := bearfamily{} b.way = "爬" travel(b)}// 输出// 人类通过汽车去旅游// 熊大通过爬去旅游

TypeScript 中的interface

总结一下,在上一节中 Go 中的interface实际上主要解决的是两个问题

  1. 一些抽象的方法不好归在一个大类里面
  2. 在函数调用时,Go 的静态语言编译校验不允许多个类型

首先看看第一个问题,我举一个比较贴近生活的例子:

现在市场上有 N 个巨头公司:阿里、腾讯、头条等等等,其中阿里、腾讯有各自的支付系统。现在要做一个电商平台需要对接这些大厂的支付系统。

type alibaba = {    支付宝: string    花呗: string    ... 以下省略N条属性}
type tencent = { 微信: string QQ支付: string ... 以下省略N条属性}
type chinaCompany = { // 通过合成一个中国公司,来往里面塞payment方法 company1: alibaba company2: tencent payment: ()=>{}}
// 以后调用方法时 可以这么做function (c: chinaCompany) {c.paymengt}

可以发现,阿里跟腾讯作为两个大巨头,我们虽然能将其再归为一个大类,里面塞支付手段,但其实不太合理,别忘了还有外国的厂商,比如苹果支付等。

那么在这种场景下,我们可以使用 interface

interface payment {  // 描述的是一类支付动作    payByPhone?: Function // 手机支付    payByFace?: Function // 刷脸支付    payBySound?: Function // 刷声音支付    ...}type alibaba = payment & {    name: string    version: string    ...}type tencent = payment & {    name: string    version: string    ...}let obj: alibaba = {  payByPhone: () => { // do something }};obj.payByPhone()

可以看到把支付方法集合放到 interface 中,是比较容易扩展的,假如未来头条也开发了支付系统,除了老的支付方式外,还新增了刷抖音支付,那么我们只需要在 payment 中加入 payByTikTok 即可,而不需要在头条这个类型中加入以前的支付方式定义。

我们再看第二个问题,这个问题在 TypeScript 也存在,别看 TS 可以允许使用|来解决,但是类型一多,咱也顶不住呀,试想一下。

// badconst goTravel = (way: bearfamily | family | birdfamily ...) => {  ...}
// goodinterface travelWay { byFoot?: Function byCar?: Function ...}const goTravel = (way: travelWay) => { ...}

虽然上面讨论的两个问题,TypeScript 中都有别的途径去扩展解决,比如说 type 与 interface 扩展,泛型等等手段。

但或许也正是因为 TypeScript 提供的这些手段,降低了 TS 中 type 与 interface 的辨识度吧🤔。

总结

经过上述的对比,个人认为可以仿造 Go 的定义,将描述方法等行为定义成 interface ,其他类型,如 int , object 等等,可以使用 type 去处理。

当然这也只是个人想法,实在分不清的情况下,我们遵循下面这个逻辑,也挺香的(手动滑稽)

如果不清楚什么时候用 interface/type,能用 interface 实现,就用 interface , 如果不能就用 type 。