为什么在TypeScript上不使用interface/type来声明业务数据结构

2024-11-10 09:01   重庆  

点击关注公众号,“技术干货” 及时达!

一、写在前面

之前我写了一篇 《也许跟大家不太一样,我是这么用TypeScript来写前端的》 有提到说 “我们不允许在TypeScript中使用 interface type 来声明业务数据结构。” 很多朋友评论想知道原因,那么我们今天来聊聊为什么。

当然,我们必须得强调一下,只是业务数据结构不允许。而如果是第三方库的数据结构,或者是第三方服务对接的数据结构,或者是装饰器的参数配置,组件的参数配置等,我们还是睁一只眼闭一只眼的,但依然不推荐。

二、为什么不使用interfacetype

1. 属性名称突入起来的转变

我们声明了 客户用户 管理员用户 部门用户 如下:

// 客户用户
interface CustomerUser {
    account: string
    name: string
    age: number
    // 更多属性
}
// 部门
interface Department{
    code: string
    name: string
    // 更多属性
}
// 超管
interface AdminUser {
    account: string
    name: string
    // 更多属性
}

// 其他使用的地方

<span>客户姓名:{{customUser.name}}</span>
<span>制单部门:{{department.name}}</
span>
<span>审批人员:{{adminUser.name}}</span>

这是前期与后端的同学约定的数据结构,前端按照这个结构已经开发完成了,等待与后端联调。

此时后端的主管扭扭捏捏的走过来,扯着自己的衣角对你说:“朋友,那个,用户的姓名字段我们想改一下,标准一点叫 realName, 你们配合改一下呗~”

你大腿一拍,虽然很生气,但为了不丢面子,说,“没问题,小事情,包在我身上。”

可是你搜索了一下 .name 的地方,有88个文件。你缩小了下搜索范围到 User.name 有66个文件。

  • 直接替换 .name.realName 肯定不行了,很多都会炸。
  • 替换 User.name 也不行了,adminUser.name 也会被替换,而且有些人命名不规范,叫 a.name,会漏掉。

咋整呢?懵了。

2. 属性类型突如其来的转变

另外一个例子,定义了用户的状态是否禁用的属性

type User{
    disable: boolean
}

// 使用的地方
if(user.disable){
    // 用户已禁用 终止逻辑
    return;
}

等你写了一大半了,后端说因为需求变了,用户的状态有 已禁用 未禁用 待禁用, 原本的布尔值支持不了需求了,要改成 number0 = 未禁用 1 = 已禁用 2 = 待禁用

先不改吧, 待禁用的用户也都无法操作了。

3. 无法为声明的业务数据扩展行为。

“长得一样”和“是”是两码事 (此项为补充。)

typeinterface 只能是声明业务数据的结构,但和对象是毫无关联的,无法使用对象内置的一些方法。

例如:

interface IUser {
  name: string
}

type TUser = {
  name: string
}

const userI: IUser = { name: 'Hamm' }
const userT: TUser = { name: 'Hamm' }
console.log(userI)
console.log(userT)

上述的 user 本身,还是个普通对象,只是满足了 User 的样子,但无法实现用户本身应有的行为,比如 eat

当然,你可能会这么搞:

interface IUser {
  name: string

  eat(): void
}

const user: IUser = {
  name: 'Hamm',
  eat() {
    console.log(`${this.name} eating...`)
  },
}

user.eat()

乍一看,用 interface 也没啥毛病是吧?

再想想。这好像是 interface 约束了行为,而赋值的地方用 Object 来临时实现了 eat 的方法?

握巢,这好像才是 interface 应该有的功能啊~

那么,这也许才是正确的例子:

class User {
  name!: string

  eat() {
    console.log(`${this.name} eating...`)
  }
}

const user = new User()
user.name = 'Hamm'
user.eat()
console.log(user)

输出的截图:

再看看上面用类方式的打印信息, 能看到数据的具名类型,也能看到对象拥有的行为,岂不是美滋滋?

4. 例子还有很多

例子还会有很多,可以参考我之前另外一篇关于数据处理的文章:TypeScript装饰器之我们是这么处理项目数据转换的

三、我们用什么?为什么?

我们统一使用 class 类 作为声明数据类型的方式,当然,之前评论区的一些问题我们也都考虑过了,那使用 class 的优势是什么?

1. 可以标记装饰器

众所周知,TypeScript 仅支持 class 类、属性、方法、参数等才能标记装饰器,至于标记装饰器了能干什么,之前我们也提到过了,可以继续翻阅本文所在专栏的其他文章。

2. 行为和属性共存

比如用户的结构,使用类可以声明属性,同时也可以直接声明方法:

class User{
nickName!: string

status!: number

isDisabled(){
return this.status === 1
}
}

然后一些行为可以在类里面统一判断,而不需要在使用的地方自行判断,更不需要单独写个方法来判断用户是否被禁用:isDisabled 从某种意义上来说,不再是用户的行为,而是一种属性了。(当然,这里可以用 getter 来实现, 更像属性,只是我们不太喜欢 getter/setter, 而是用具体的方法。可以参考 Getter/Setter中那些有意思的小故事 )

因为可以写方法,我们可以利用 构造方法、链式调用等各种类的方式来完成很多好玩的需求,让写代码也可以变得很舒服。

3. 可以使用抽象类和抽象属性来实现更多设计模式

设计模式有多好玩相信很多后端开发者是很清楚的,当然,设计模式在前端也可以很好玩。

之前也有说过,TypeScript 比 Java 有意思的一个地方,就是 TypeScript 竟然把属性也可以给抽象掉~

4. 还要很多。。。

还有很多很多的好处,但这里必须要强调的一点是,很多人拿面向对象编程方式和函数式编程方式来抵触面向对象在前端存在的必要性,这里说明一下:

编程范式只是一种方式而已。

前端在函数式编程上加上面向对象,可能会更好玩,而且更有效率。

很多人说前端加上面向对象,后来的人不会维护,看不懂。

我一直没想过这个问题,今天思考了一下:

他看不懂关我啥事?面向对象很难吗就看不懂了?

四、我们用interfacetype了吗

用了。

interface 大量的用在了设计模式以及行为约束上,用于数据结构的只有一个地方,那就是装饰器的参数。

type 也用了,但大多数是用来起别名。。。

至于其他的数据结构,比如与第三方对接,那就用 Json 呗。用 interface 还有必要吗?

哦对了,我们封装了一个好玩的 interface, 叫 IJson,用来解决写 AnyScript 的问题:

export interface IJson<V = any> {
  /**
   * JSON的键
   */

  [x: string]: V;
}

希望你能看懂上面的讽刺代码。

五、全文完

相关前端面向对象开发的源代码可以参考:

Github: https://github.com/HammCn/AirPower4T

Gitee: https://gitee.com/air-power/AirPower4T

当然,也欢迎你阅读我的专栏:“用TypeScript写前端”。

点击关注公众号,“技术干货” 及时达!

稀土掘金技术社区
掘金,一个帮助开发者成长的技术社区
 最新文章