为什么 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 stringObject.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.values
或 Object.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]); }});
延伸阅读
- 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