為什麼 TypeScript 中 Object.keys 返回 string[] 型別?
前言
TypeScript 存在一些不太直覺但背後卻有合理因素的問題要留意,像是無論傳入任何物件進 Object.keys
都仍會回傳 string[]
型別是其中之一,關連到 JavaScript 本身的特性與 TypeScript 使用結構型別系統的關係,本文探討背後因素與解套方法。
Object.keys
只會返回 string[]
?
如果到 TypeScript es5.d.ts 會發現 Object.keys
回傳型別被定義為 string[]
也就是說當我們撰寫以下代碼時會遇到錯誤: Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{ name: string; age: number; }'. No index signature with a parameter of type 'string' was found on type '{ name: string; age: number; }'.
解析錯誤訊息得知 person
會需要接受特定值的 key
而非任意 string
,那背後麼又是什麼原因 Object.keys
被設計成返回更為鬆散的 string[]
型別呢?
背後原因
因為 JavaScript 物件可以在 「執行時動態改變其內容」。
TypeScript 為了考量和 JavaScript 的動態特性更加契合,因而使用結構型別系統(Structural type system),也就是說它著重在型別的結構組成,而不是型別名稱,如果兩個型別組成相同那它們就是相同的型別(鴨子型別 Duck Typing)。
以上鴨子型別範例代碼將發生錯誤:Property 'bar' is missing in type 'B' but required in type 'A'.
,也就說 如果一個型別包含了另一個型別所需的所有屬性,它可以被賦值給那個型別 (b2
範例)。
如果我們將 Object.keys
設計成 keys<T extends object>(o: T): (keyof T)[]
來返回物件的鍵類型,這種類型定義在編譯時可能看起來是正確的,但在運行時會產生問題。因為 JavaScript 物件的屬性可以被動態添加或刪除,物件在運行時的實際屬性可能與編譯時的類型定義 T
不符。因此,TypeScript 選擇返回 string[]
類型,這樣能更準確地反映 JavaScript 的動態特性。
解方一:映射型別
要解決這個問題最簡單就是映射對應的型別上去,告訴 TypeScript 我清楚知道 key
的型別是 keyof typeof person
,雖然這麼做等於犧牲 TypeScript 回傳 string[]
的意義,但通常也夠用且能快速解決問題。
解方二:使用現成方法
像上個範例,如果要存取物件中的值其實可以直接使用 Object.values
或 Object.entries
都是不錯的選擇,並不一定要透過 key
去取得對應的物件項目。
解方三:Type Guard
為了最大程度的型別安全,我們可以添加 type predicates 來檢測 Object.keys
是否為特定型別:
延伸閱讀
- Why Object.keys Returns an Array of Strings in TypeScript (And How To Fix It) - Matt Stobbs
- Why doesn’t TypeScript properly type Object.keys? - Alex Harri
- Why doesn’t TypeScript properly type Object.keys? (alexharri.com) - Hacker News