Why Object.keys returns string[] in TypeScript

為什麼 TypeScript 中 Object.keys 返回 string[] 型別?

前言

TypeScript 存在一些不太直覺但背後卻有合理因素的問題要留意,像是無論傳入任何物件進 Object.keys 都仍會回傳 string[] 型別是其中之一,關連到 JavaScript 本身的特性與 TypeScript 使用結構型別系統的關係,本文探討背後因素與解套方法。

Object.keys 只會返回 string[]

如果到 TypeScript es5.d.ts🔗 會發現 Object.keys 回傳型別被定義為 string[]

/**
* Returns the names of the enumerable string properties and methods of an object.
* @param o Object that contains the properties and methods. This can be an object that you created or an existing Document Object Model (DOM) object.
*/
keys(o: object): 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; }'.

const person = {
name: 'joe',
age: 20,
};
// Object.keys(person) is string[]
// key is string
Object.keys(person).forEach((key) => {
console.log(person[key]);
});

解析錯誤訊息得知 person 會需要接受特定值的 key 而非任意 string,那背後麼又是什麼原因 Object.keys 被設計成返回更為鬆散的 string[] 型別呢?

背後原因

因為 JavaScript 物件可以在 「執行時動態改變其內容」

TypeScript 為了考量和 JavaScript 的動態特性更加契合,因而使用結構型別系統(Structural type system),也就是說它著重在型別的結構組成,而不是型別名稱,如果兩個型別組成相同那它們就是相同的型別(鴨子型別 Duck Typing)。

type A = { foo: number; bar: number };
type B = { foo: number };
const a1: A = { foo: 1, bar: 2 };
const b1: B = { foo: 3 };
const b2: B = a1;
const a2: A = b1;

以上鴨子型別範例代碼將發生錯誤: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.keys(person).forEach((key) => {
console.log(person[key as keyof typeof person]);
});

解方二:使用現成方法

像上個範例,如果要存取物件中的值其實可以直接使用 Object.valuesObject.entries 都是不錯的選擇,並不一定要透過 key 去取得對應的物件項目。

Object.values(person).forEach((key) => {
console.log(key);
});

解方三:Type Guard

為了最大程度的型別安全,我們可以添加 type predicates🔗 來檢測 Object.keys 是否為特定型別:

const person = {
name: 'joe',
age: 20,
};
function isPersonKey(value: string): value is keyof typeof person {
return Object.keys(person).includes(value);
}
Object.keys(person).forEach((key) => {
if (isPersonKey(key)) {
console.log(person[key]);
}
});

延伸閱讀