
简单给大家说一下案例背景。
我在写我的付费专栏「图解算法」时,准备基于拉链法实现一个 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

被折叠的 条评论
为什么被折叠?



