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]);
}
});

延伸阅读