前言
大概半年之前学习了一下 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 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实际上主要解决的是两个问题
- 一些抽象的方法不好归在一个大类里面
- 在函数调用时,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 可以允许使用|
来解决,但是类型一多,咱也顶不住呀,试想一下。
// bad
const goTravel = (way: bearfamily | family | birdfamily ...) => {
...
}
// good
interface 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 。