TypeScript 之痛,为啥很多人会用成 anyScript

简单给大家说一下案例背景。

我在写我的付费专栏「图解算法」时,准备基于拉链法实现一个 HashMap 对象,并且准备用环形链表来存储碰撞到一起的输入值。

完整的存储结果,大家可以通过上图来理解。

由于环形链表的算法结构,在 React 底层原理中被大量运用,因此,是大厂面试的高频考题之一。而哈希碰撞 + 环形链表则是一个综合性较强的场景,是一个考察高级开发候选人的基础是否扎实的比较合适的题目,各位面试官也可以借鉴

然而,在实现这个代码的过程中,我却在一个 TypeScript 的类型问题上遇到了麻烦。

按照既有的思路,我首先需要定义一个链表节点的类型,该节点存储 key-value 键值对。这里由于 Map 支持的 key 值是任意类型,value 的类型也不确定,因此,我的本能反应,就是使用 any 来约定类型,然后再加一个指向下一个节点的指针 next

type HNode = {
  key: any,
  value: any,
  next: HNode | null
}

但是很明显啊,直接用 any 肯定是不那么专业的,因此呢,我就不得不花额外的精力去思考到底应该用什么样的类型比较合适。

由于 key-value 都是从外部传入的,因此比较标准一点的做法,是可以传入泛型变量来站位

可以改造成这样

type HNode<K, V> = {
  key: K,
  value: V,
  next: HNode<K, V> | null
}

但是,改造成这样之后呢,麻烦的事情就来了。他与 Map 的语法定义就不匹配了。为啥我要这么说呢?

我们都知道,在使用 Map 的时候,如果我们传入两个 key-value 的值进来,这两个键值对的 key 类型与 value 类型,在语法上,是没有强制要求他们必须是相同的。

例如,在同一个 Map 中,我们可以分别存储如下两个键值对

const m = new Map<any, any>()
m.set('tom', {})
m.set(1024, 'hello world!')

但是,我们一旦用泛型来约束之后,这样写就难受了呀,最终还是只能传入 any 才能解决问题。

当然,这是一方面,另外一方面,你会发现,一个简单的场景,为了解决类型问题,你又多花了大量的时间。要完美解决的话,又得引入重载,这运用成本就嘎嘎嘎的往上升。

最后发现,还是 any 省事儿!

后面还有一个场景,如下代码所示,我在编写 set 方法,先简单瞄一眼代码,后面再分析。

// 插入键值对
set(key: K, value: V) {
const index = this.hash(key);
const bucket = this.buckets[index]

const node: HNode<K, V> = {
    key: key,
    value: value,
    next: null
  }

// 如果桶是空的,初始化一个链表
if (!bucket) {
    node.next = node
    this.buckets[index] = {
      length: 1,
      last: node
    };
    return
  }

const last = bucket.last
const first = last?.next

// 检查是否已存在相同的key
let current: HNode<K, V> = bucket.last
while(true) {
    if (current.key === key) {
      current.value = value
    }
    current = current.next
    if (current === bucket.last) {
      break
    }
  }

// add the node to last
  last.next = node
  node.next = first
  bucket.last = node
  bucket.length += 1
}

set 的语法如下

const m = new HashMap()
m.set(1001, {})

在底层实现中,由于我们需要将键值对插入到链表中,因此在插入之前,就需要先定义好节点。这里的一个问题就是,我这个节点的指针类型的值,应该是什么?

默认理所应当应该是 null,因此此时还不知道会指向谁呢?

const node: HNode<K, V> = {
  key: key,
  value: value,
  next: null
}

但是,真实的场景是,由于我们使用的是环形链表,因此这里的 next 实际上是总有值的,最差也是指向自身。只要有节点存在,他就不可能为 null

node.next = node

因此,如何要和实际情况匹配的话,我们就不应该将其设置为 null。

但是由于在初始化时,语法不允许直接指向自身,所以这里就不得不先将其设置为 null,在根据条件判断来确定指向

但是,这就会引发后续的麻烦,后续当我要获取一个节点时,由于真实情况是,我一定能获取到一个节点,不会存在获取到为 null 的情况,但是由于我们在类型的定义上,将其设置为了 null,这个时候,就不得不额外处理获取值为 null 的情况

这特么代码写起来就贼难受。

我就只能用 as 来强制约定类型

current = current.next as HNode<K, V>

或者干脆就直接在定义的时候,全给写成 any,就啥麻烦事儿都没有了

type HNode = {
  key: any,
  value: any,
  next: any
}

总结

TypeScript 作为类型约束的语言,在套在强调类型灵活性的 JavaScript 上时,在实际的使用过程中是有非常多的镇痛的,甚至有很多即使用了类型体操都无法解决的问题,因为他们从底层本质上来说,确实有许多无法统一的逻辑和场景。

这是许多人把 ts 用成 anyScript 的原因。

当然,适当使用 any,绝对不是技术水平不行的表现。我们要学会在使用成本和类型约束之间,做一个合适的取舍。一方面我们要适当约束 JS 的灵活性以迎合 TS 的规则,另外一方面,我们也要适当使用 any 释放 TS 的灵活性,来降低开发成本。否则,我们的开发体感就会变得很差,会被 TypeScript 搞的很难受。


点击阅读原文,了解我的个人网站 usehook.cn

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值