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 搞的很難受。