为什么在Ts中Object.keys总是返回字符串数组?

一个常见的问题

在很多情况下,我们需要遍历一个对象的所有Key,就像这样

1
2
3
4
5
6
7
let marks = {
1: 'a',
2: 'b'
}

const values = Object.keys(marks).map(key=>marks[key])
// error: 元素隐式具有 "any" 类型,因为类型为 "string" 的表达式不能用于索引类型 "{ 1: string; 2: string; }"。在类型 "{ 1: string; 2: string; }" 上找不到具有类型为 "string" 的参数的索引签名。

Ts会立即报错,因为Object.keys的类型是这样

1
2
3
interface ObjectConstructor {
keys(o: object): string[];
}

这种时候我们往往需要对key进行断言

1
2
3
4
5
6
7
let marks = {
1: 'a',
2: 'b'
}

const values = Object.keys(marks).map(key=>marks[key as `${keyof typeof marks}`])
// no error

这段代码只是简单的对类型的断言,更准确的断言实际还应该排除symbol类型,因为对象的键也可能是symbol,但是Object.keys只会遍历对象的可枚举的字符串属性。

同时这里也将数字转换为字符串,因为对象的键除了symbol,都会被转换为字符串。

为什么Ts没有处理?

在GitHub上事实上有很多讨论,其中一个Pull request的讨论下有详细的回答。

Ts作者说明了使用key of T仅仅只在类型系统中工作,在运行时,类型会被抹除掉,而在实际场景,往往对象会拥有更多的key,这时类型是不安全的。

特别的,在fon in循环中,如果被推断的对象是一个泛型参数,Ts中将其key推断为 Extract<keyof T, string>,而在其它情况下,总是会被推断为string

1
2
3
4
5
6
7
const testCases = <T>(object: T)=> {
for (const key in object) {
if (Object.prototype.hasOwnProperty.call(object, key)) {
const element = object[key]; // key: Extract<keyof T, string>
}
}
}

这里其实还是不太明白为什么使用泛型参数可以,而使用其他类型不行,可能与泛型参数是根据输入的类型自动推断有关。

其他的相关函数

其他的对象遍历函数基本与Object.keys表现一致,只是可以通过泛型传递值的类型。

1
2
3
4
5
6
interface ObjectConstructor {
values<T>(o: { [s: string]: T } | ArrayLike<T>): T[];
values(o: {}): any[];
entries<T>(o: { [s: string]: T } | ArrayLike<T>): [string, T][];
entries(o: {}): [string, any][];
}

小结

这点实际上是由于Ts类型仅存在编译时,和运行时的类型是无关的,所以在这种依赖于运行时的函数时,无法保证类型安全,因此给了一个最宽泛的string并让开发者自己断言。

不过在问题的讨论下,也有举例Readonly的评论,因为Readonly实际也仅存在编译类型检查时,运行时也仍然是可写的。