前言
大概半年之前学习了一下 TypeScript,那时候对其中的interface和type就产生了疑问,它们具体有什么区别?比较火的一个讲解是这篇文章: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 类型可以定义一组方法,但是这些不需要实现。请注意:此处限定是一组方法,既然是方法,就不能是变量;而且是一组,表明可以有多个方法。
如何理解?
- interface 是一种类型
- interface 只关心方法
为什么会有 interface?
这里先说 2 个 Go 的背景:
- Go 也是有type的,也可以用于做interface类似的事情。
- Go 函数接受参数时,不能像 TS 那样使用
function(arg: string | number)来接受形参
基于背景 2,我们来思考一种场景,假如我们现在要出门旅行了,选择了一种交通工具出发,交通工具有汽车、飞机...,每种出发方式不同,基于这个场景,我们来写写代码。
import "fmt"type family struct{// 这里struct 可以理解为 JS中的对象// 我定义了一个家庭出游的对象father stringmother stringson stringway stringgoTravel 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 stringgoTravel 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实际上主要解决的是两个问题
- 一些抽象的方法不好归在一个大类里面
- 在函数调用时,Go 的静态语言编译校验不允许多个类型
首先看看第一个问题,我举一个比较贴近生活的例子:
现在市场上有 N 个巨头公司:阿里、腾讯、头条等等等,其中阿里、腾讯有各自的支付系统。现在要做一个电商平台需要对接这些大厂的支付系统。
type alibaba = {支付宝: string花呗: string... 以下省略N条属性}type tencent = {微信: stringQQ支付: string... 以下省略N条属性}type chinaCompany = { // 通过合成一个中国公司,来往里面塞payment方法company1: alibabacompany2: tencentpayment: ()=>{}}// 以后调用方法时 可以这么做function (c: chinaCompany) {c.paymengt}
可以发现,阿里跟腾讯作为两个大巨头,我们虽然能将其再归为一个大类,里面塞支付手段,但其实不太合理,别忘了还有外国的厂商,比如苹果支付等。
那么在这种场景下,我们可以使用 interface。
interface payment { // 描述的是一类支付动作payByPhone?: Function // 手机支付payByFace?: Function // 刷脸支付payBySound?: Function // 刷声音支付...}type alibaba = payment & {name: stringversion: string...}type tencent = payment & {name: stringversion: string...}let obj: alibaba = {payByPhone: () => { // do something }};obj.payByPhone()
可以看到把支付方法集合放到 interface 中,是比较容易扩展的,假如未来头条也开发了支付系统,除了老的支付方式外,还新增了刷抖音支付,那么我们只需要在 payment 中加入 payByTikTok 即可,而不需要在头条这个类型中加入以前的支付方式定义。
我们再看第二个问题,这个问题在 TypeScript 也存在,别看 TS 可以允许使用|来解决,但是类型一多,咱也顶不住呀,试想一下。
// badconst goTravel = (way: bearfamily | family | birdfamily ...) => {...}// goodinterface travelWay {byFoot?: FunctionbyCar?: Function...}const goTravel = (way: travelWay) => {...}
虽然上面讨论的两个问题,TypeScript 中都有别的途径去扩展解决,比如说 type 与 interface 扩展,泛型等等手段。
但或许也正是因为 TypeScript 提供的这些手段,降低了 TS 中 type 与 interface 的辨识度吧🤔。
总结
经过上述的对比,个人认为可以仿造 Go 的定义,将描述方法等行为定义成 interface ,其他类型,如 int , object 等等,可以使用 type 去处理。
当然这也只是个人想法,实在分不清的情况下,我们遵循下面这个逻辑,也挺香的(手动滑稽)
如果不清楚什么时候用 interface/type,能用 interface 实现,就用 interface , 如果不能就用 type 。