TypeScript 之痛,為啥很多人會(huì)用成 AnyScript
簡(jiǎn)單給大家說(shuō)一下案例背景。
我在寫(xiě)我的付費(fèi)專(zhuān)欄「圖解算法」時(shí),準(zhǔn)備基于拉鏈法實(shí)現(xiàn)一個(gè) HashMap 對(duì)象,并且準(zhǔn)備用環(huán)形鏈表來(lái)存儲(chǔ)碰撞到一起的輸入值。
完整的存儲(chǔ)結(jié)果,大家可以通過(guò)上圖來(lái)理解。
?
由于環(huán)形鏈表的算法結(jié)構(gòu),在 React 底層原理中被大量運(yùn)用,因此,是大廠面試的高頻考題之一。而哈希碰撞 + 環(huán)形鏈表則是一個(gè)綜合性較強(qiáng)的場(chǎng)景,是一個(gè)考察高級(jí)開(kāi)發(fā)候選人的基礎(chǔ)是否扎實(shí)的比較合適的題目,各位面試官也可以借鑒
然而,在實(shí)現(xiàn)這個(gè)代碼的過(guò)程中,我卻在一個(gè) TypeScript 的類(lèi)型問(wèn)題上遇到了麻煩。
按照既有的思路,我首先需要定義一個(gè)鏈表節(jié)點(diǎn)的類(lèi)型,該節(jié)點(diǎn)存儲(chǔ) key-value 鍵值對(duì)。這里由于 Map 支持的 key 值是任意類(lèi)型,value 的類(lèi)型也不確定,因此,我的本能反應(yīng),就是使用 any 來(lái)約定類(lèi)型,然后再加一個(gè)指向下一個(gè)節(jié)點(diǎn)的指針 next
type HNode = {
key: any,
value: any,
next: HNode | null
}但是很明顯啊,直接用 any 肯定是不那么專(zhuān)業(yè)的,因此呢,我就不得不花額外的精力去思考到底應(yīng)該用什么樣的類(lèi)型比較合適。
由于 key-value 都是從外部傳入的,因此比較標(biāo)準(zhǔn)一點(diǎn)的做法,是可以傳入泛型變量來(lái)站位
可以改造成這樣
type HNode<K, V> = {
key: K,
value: V,
next: HNode<K, V> | null
}但是,改造成這樣之后呢,麻煩的事情就來(lái)了。他與 Map 的語(yǔ)法定義就不匹配了。為啥我要這么說(shuō)呢?
我們都知道,在使用 Map 的時(shí)候,如果我們傳入兩個(gè) key-value 的值進(jìn)來(lái),這兩個(gè)鍵值對(duì)的 key 類(lèi)型與 value 類(lèi)型,在語(yǔ)法上,是沒(méi)有強(qiáng)制要求他們必須是相同的。
例如,在同一個(gè) Map 中,我們可以分別存儲(chǔ)如下兩個(gè)鍵值對(duì)
const m = new Map<any, any>()
m.set('tom', {})
m.set(1024, 'hello world!')但是,我們一旦用泛型來(lái)約束之后,這樣寫(xiě)就難受了呀,最終還是只能傳入 any 才能解決問(wèn)題。
當(dāng)然,這是一方面,另外一方面,你會(huì)發(fā)現(xiàn),一個(gè)簡(jiǎn)單的場(chǎng)景,為了解決類(lèi)型問(wèn)題,你又多花了大量的時(shí)間。要完美解決的話(huà),又得引入重載,這運(yùn)用成本就嘎嘎嘎的往上升。

最后發(fā)現(xiàn),還是 any 省事兒!
后面還有一個(gè)場(chǎng)景,如下代碼所示,我在編寫(xiě) set 方法,先簡(jiǎn)單瞄一眼代碼,后面再分析。
// 插入鍵值對(duì)
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
}
// 如果桶是空的,初始化一個(gè)鏈表
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 的語(yǔ)法如下
const m = new HashMap()
m.set(1001, {})在底層實(shí)現(xiàn)中,由于我們需要將鍵值對(duì)插入到鏈表中,因此在插入之前,就需要先定義好節(jié)點(diǎn)。這里的一個(gè)問(wèn)題就是,我這個(gè)節(jié)點(diǎn)的指針類(lèi)型的值,應(yīng)該是什么?
默認(rèn)理所應(yīng)當(dāng)應(yīng)該是 null,因此此時(shí)還不知道會(huì)指向誰(shuí)呢?
const node: HNode<K, V> = {
key: key,
value: value,
next: null
}但是,真實(shí)的場(chǎng)景是,由于我們使用的是環(huán)形鏈表,因此這里的 next 實(shí)際上是總有值的,最差也是指向自身。只要有節(jié)點(diǎn)存在,他就不可能為 null
node.next = node因此,如何要和實(shí)際情況匹配的話(huà),我們就不應(yīng)該將其設(shè)置為 null。
但是由于在初始化時(shí),語(yǔ)法不允許直接指向自身,所以這里就不得不先將其設(shè)置為 null,在根據(jù)條件判斷來(lái)確定指向

但是,這就會(huì)引發(fā)后續(xù)的麻煩,后續(xù)當(dāng)我要獲取一個(gè)節(jié)點(diǎn)時(shí),由于真實(shí)情況是,我一定能獲取到一個(gè)節(jié)點(diǎn),不會(huì)存在獲取到為 null 的情況,但是由于我們?cè)陬?lèi)型的定義上,將其設(shè)置為了 null,這個(gè)時(shí)候,就不得不額外處理獲取值為 null 的情況

這特么代碼寫(xiě)起來(lái)就賊難受。
我就只能用 as 來(lái)強(qiáng)制約定類(lèi)型
current = current.next as HNode<K, V>或者干脆就直接在定義的時(shí)候,全給寫(xiě)成 any,就啥麻煩事兒都沒(méi)有了
type HNode = {
key: any,
value: any,
next: any
}總結(jié)
TypeScript 作為類(lèi)型約束的語(yǔ)言,在套在強(qiáng)調(diào)類(lèi)型靈活性的 JavaScript 上時(shí),在實(shí)際的使用過(guò)程中是有非常多的鎮(zhèn)痛的,甚至有很多即使用了類(lèi)型體操都無(wú)法解決的問(wèn)題,因?yàn)樗麄儚牡讓颖举|(zhì)上來(lái)說(shuō),確實(shí)有許多無(wú)法統(tǒng)一的邏輯和場(chǎng)景。
這是許多人把 ts 用成 anyScript 的原因。
當(dāng)然,適當(dāng)使用 any,絕對(duì)不是技術(shù)水平不行的表現(xiàn)。我們要學(xué)會(huì)在使用成本和類(lèi)型約束之間,做一個(gè)合適的取舍。一方面我們要適當(dāng)約束 JS 的靈活性以迎合 TS 的規(guī)則,另外一方面,我們也要適當(dāng)使用 any 釋放 TS 的靈活性,來(lái)降低開(kāi)發(fā)成本。否則,我們的開(kāi)發(fā)體感就會(huì)變得很差,會(huì)被 TypeScript 搞的很難受。



























