The current return type (string[]
) is intentional. Why?
当前返回类型(字符串[])是有意的。为什么?
Consider some type like this:
考虑一下这样的类型:
interface Point {
x: number;
y: number;
}
You write some code like this:
您可以编写一些代码,如下所示:
function fn(k: keyof Point) {
if (k === "x") {
console.log("X axis");
} else if (k === "y") {
console.log("Y axis");
} else {
throw new Error("This is impossible");
}
}
Let's ask a question:
让我们问一个问题:
In a well-typed program, can a legal call to fn
hit the error case?
The desired answer is, of course, "No". But what does this have to do with Object.keys
?
当然,人们希望得到的答案是“不”。但这与Object.key有什么关系呢?
Now consider this other code:
现在考虑下面的另一段代码:
interface NamedPoint extends Point {
name: string;
}
const origin: NamedPoint = { name: "origin", x: 0, y: 0 };
Note that according to TypeScript's type system, all NamedPoint
s are valid Point
s.
注意,根据TypeScrip的类型系统,所有NamedPoint都是有效的点。
Now let's write a little more code:
现在,让我们再编写一些代码:
function doSomething(pt: Point) {
for (const k of Object.keys(pt)) {
// A valid call if Object.keys(pt) returns (keyof Point)[]
fn(k);
}
}
// Throws an exception
doSomething(origin);
Our well-typed program just threw an exception!
我们的类型良好的程序刚刚抛出了一个异常!
Something went wrong here!
By returning keyof T
from Object.keys
, we've violated the assumption that keyof T
forms an exhaustive list, because having a reference to an object doesn't mean that the type of the reference isn't a supertype of the type of the value.
这里出了点问题!通过从对象键返回key of T,我们违反了key of T形成详尽列表的假设,因为拥有对对象的引用并不意味着引用的类型不是值类型的超类型。
Basically, (at least) one of the following four things can't be true:
基本上,(至少)以下四件事中的一件不可能是真的:
keyof T
is an exhaustive list of the keys of T
- A type with additional properties is always a subtype of its base type
- It is legal to alias a subtype value by a supertype reference
Object.keys
returns keyof T
Throwing away point 1 makes keyof
nearly useless, because it implies that keyof Point
might be some value that isn't "x"
or "y"
.
丢弃Point1使key of几乎毫无用处,因为它意味着key of Point可能是不是“x”或“y”的某个值。
Throwing away point 2 completely destroys TypeScript's type system. Not an option.
扔掉第二点完全摧毁了打字稿的字体系统。没有选择的余地。
Throwing away point 3 also completely destroys TypeScript's type system.
扔掉第三点也会彻底摧毁打字稿的字体系统。
Throwing away point 4 is fine and makes you, the programmer, think about whether or not the object you're dealing with is possibly an alias for a subtype of the thing you think you have.
丢弃第4点是可以的,这会让您(程序员)考虑您正在处理的对象是否可能是您认为您拥有的东西的子类型的别名。
The "missing feature" to make this legal but not contradictory is Exact Types, which would allow you to declare a new kind of type that wasn't subject to point #2. If this feature existed, it would presumably be possible to make Object.keys
return keyof T
only for T
s which were declared as exact.
使这一点合法但不矛盾的“缺失功能”是精确类型,这将允许您声明一种不受第2点约束的新类型。如果存在此功能,可能会使Object.key仅为声明为精确的类型返回key of T。
Addendum: Surely generics, though?
Commentors have implied that Object.keys
could safely return keyof T
if the argument was a generic value. This is still wrong. Consider:
评论者暗示,如果参数是泛型值,则Object.key可以安全地返回key of T。这仍然是错误的。请考虑:
class Holder<T> {
value: T;
constructor(arg: T) {
this.value = arg;
}
getKeys(): (keyof T)[] {
// Proposed: This should be OK
return Object.keys(this.value);
}
}
const MyPoint = { name: "origin", x: 0, y: 0 };
const h = new Holder<{ x: number, y: number }>(MyPoint);
// Value 'name' inhabits variable of type 'x' | 'y'
const v: "x" | "y" = (h.getKeys())[0];
or this example, which doesn't even need any explicit type arguments:
或者这个示例,它甚至不需要任何显式类型参数:
function getKey<T>(x: T, y: T): keyof T {
// Proposed: This should be OK
return Object.keys(x)[0];
}
const obj1 = { name: "", x: 0, y: 0 };
const obj2 = { x: 0, y: 0 };
// Value "name" inhabits variable with type "x" | "y"
const s: "x" | "y" = getKey(obj1, obj2);
For a workaround in cases when you're confident that there aren't extra properties in the object you're working with, you can do this:
对于您确信正在使用的对象中没有额外属性的情况下的一种解决方法,您可以这样做:
const obj = {a: 1, b: 2}
const objKeys = Object.keys(obj) as Array<keyof typeof obj>
// objKeys has type ("a" | "b")[]
You can extract this to a function if you like:
如果您愿意,可以将其提取到一个函数中:
const getKeys = <T>(obj: T) => Object.keys(obj) as Array<keyof T>
const obj = {a: 1, b: 2}
const objKeys = getKeys(obj)
// objKeys has type ("a" | "b")[]
As a bonus, here's Object.entries
, pulled from a GitHub issue with context on why this isn't the default:
作为额外的好处,以下是从GitHub的一个问题中摘录的对象条目,其中包含关于为什么这不是默认设置的问题:
type Entries<T> = {
[K in keyof T]: [K, T[K]]
}[keyof T][]
function entries<T>(obj: T): Entries<T> {
return Object.entries(obj) as any;
}
This is the top hit on google for this type of issue, so I wanted to share some help on moving forwards.
这是谷歌在这类问题上的热门话题,所以我想分享一些关于继续前进的帮助。
These methods were largely pulled from the long discussions on various issue pages which you can find links to in other answers/comment sections.
这些方法很大程度上是从各种问题页面上的长时间讨论中提取的,您可以在其他答案/评论部分找到链接。
So, say you had some code like this:
因此,假设您有如下代码:
const obj = {};
Object.keys(obj).forEach((key) => {
obj[key]; // blatantly safe code that errors
});
Here are a few ways to move forwards:
以下是一些前进的方法:
If you don't need the keys and really just need the values, use .entries()
or .values()
instead of iterating over the keys.
const obj = {};
Object.values(obj).forEach(value => value);
Object.entries(obj).forEach([key, value] => value);
Create a helper function:
function keysOf<T extends Object>(obj: T): Array<keyof T> {
return Array.from(Object.keys(obj)) as any;
}
const obj = { a: 1; b: 2 };
keysOf(obj).forEach((key) => obj[key]); // type of key is "a" | "b"
Re-cast your type (this one helps a lot for not having to rewrite much code)
const obj = {};
Object.keys(obj).forEach((_key) => {
const key = _key as keyof typeof obj;
obj[key];
});
Which one of these is the most painless is largely up to your own project.
其中哪一个是最无痛的,很大程度上取决于你自己的项目。
Possible solution
可能的解决方案
const isName = <W extends string, T extends Record<W, any>>(obj: T) =>
(name: string): name is keyof T & W =>
obj.hasOwnProperty(name);
const keys = Object.keys(x).filter(isName(x));
I had this issue too, so I wrote some typed functions.
我也有这个问题,所以我编写了一些类型化函数。
Knowing that Object.keys
and Object.entries
return all keys as string
I created a ToStringKey
type:
知道了Object.key和Object.Entries以字符串形式返回所有键的情况下,我创建了一个ToStringKey类型:
/**
* Returns the names of the _typed_ enumerable string properties and methods of an object.
*
* Note: Limiting Object.keys to a specific type may lead to inconsistencies between type-checking and runtime behavior.
* Use this function when you are certain of the objects keys.
*/
export const getTypedKeys = Object.keys as <T extends object>(
obj: T
// Using `ToStringKey` because Object.keys returns all keys as strings.
) => Array<ToStringKey<T>>;
/**
* Returns an array of _typed_ values of the enumerable properties of an object.
*/
export const getTypedValues = Object.values as <T extends object>(obj: T) => Array<T[keyof T]>;
/**
* Returns an array of _typed_ key/values of the enumerable properties of an object.
*
* Note: Limiting Object.entries to a specific type may lead to inconsistencies between type-checking and runtime behavior.
* Use this function when you are certain of the objects keys.
*/
export const getTypedEntries = Object.entries as <T extends object>(
obj: T
// Using `ToStringKey` because Object.entries returns all keys as strings.
) => Array<[ToStringKey<T>, T[keyof T]]>;
/**
* Converts object keys to their string literal types.
*/
type ToStringKey<T> = `${Extract<keyof T, string | number>}`;
I do not recommend defining these method types globally. Create separate utility functions instead.
我不建议全局定义这些方法类型。取而代之的是创建单独的实用函数。
While TypeScript can infer and work with types, it cannot determine runtime-specific characteristics like enumerability.
虽然TypeScrip可以推断和处理类型,但它不能确定运行时特定的特征,如可枚举性。
just do this and the problem is gone
只要这样做,问题就解决了
declare global {
interface ObjectConstructor {
keys<T>(o: T): (keyof T)[]
// @ts-ignore
entries<U, T>(o: { [key in T]: U } | ArrayLike<U>): [T, U][]
}
}
I added // @ts-ignore because ts would tell me this:
我添加了//@ts-Ignore,因为ts会告诉我:
Type 'T' is not assignable to type 'string | number | symbol
If someone have a solution to get rid of // @ts-ignore without loosing the ability to preserve the dynamic aspect of T, let us know in the comments
如果有人有一个解决方案可以在不失去保持T的动态方面的能力的情况下摆脱// @ts-ignore,请在评论中告诉我们。
If this breaks your code you can do:
如果这破坏了您的代码,您可以执行以下操作:
Object.tsKeys = function getObjectKeys<Obj>(obj: Obj): (keyof Obj)[] {
return Object.keys(obj!) as (keyof Obj)[]
}
// @ts-ignore
Object.tsEntries = function getObjectEntries<U, T>(obj: { [key in T]: U }): [T, U][] {
return Object.entries(obj!) as unknown as [T, U][]
}
declare global {
interface ObjectConstructor {
// @ts-ignore
tsEntries<U, T>(o: { [key in T]: U }): [T, U][]
tsKeys<T>(o: T): (keyof T)[]
}
}
更多回答
However, there is common case when point 3. is excluded, when for example T
is inferred and is guaranteed to be precise: const f: <T>(t: T) => void = (t) => { Object.keys(t).forEach(k => t[k]) }
. I have lots of places like that in my code, where I really want Object.keys()
to return (keyof T)[].
然而,当排除点3.时存在常见情况,例如,当T被推断并且被保证是精确的:const f:(t:t)=>void=(T)=>{Object.key(T).forEach(k=>t[k])}。我的代码中有很多这样的地方,我真的希望Object.key()返回(Key Of T)[]。
As arthem also points out, the confusion comes from the fact that 9 out of 10 times you will end up in some way using a type assertion to keyof T
to do anything useful with the result of keys
. You might argue it is better to be explicit about it so you are more aware of the risk you are taking, but probably 9/10 devs will just add the type assertion and not be aware of the issues you highlight ..
正如ArThem还指出的那样,混淆来自这样一个事实,即10次中有9次您将以某种方式使用key of T的类型断言来对key的结果做任何有用的事情。您可能会争辩说,最好是明确地说明这一点,以便更清楚地意识到您正在承担的风险,但9/10开发人员可能只会添加类型断言,而不会意识到您强调的问题。
Why can't Object.keys<T>(c extends T obj)
simply filter the keys on obj (type c) returning keys of T?
为什么不能简单地对返回T的键的obj(类型c)过滤键呢?
If anyone has actually been screwed up by type-casting Object.keys(foo) to Array<keyof typeof foo>, where the runtime value from Object.keys actually included more keys than were know at compile-time, Many people would love to see this code as an example. Please share it
如果有人真的被类型强制转换为数组搞砸了,其中来自对象键的运行时值实际上包含比编译时所知的更多的键,很多人都希望看到这段代码作为一个例子。请分享一下
Often, this is solely used to be able to loop through an object but that makes it impossible since Object.keys(product).forEach((key) => { // do something with product[key] but leads to error 'has type any because string cannot be used to query ...' });
will lead to an error.
通常,这仅仅是为了能够在对象中循环,但这使得它不可能,因为Object.keys(product).forEach((key)=> { //对product[key]做一些事情,但导致错误'has type any because string cannot be used to query. ' });将导致错误。
I recently banged my head against this and would like to toss one more option on to the heap: convert over to a Map. It's a huge hassle to convert over to Map for supporting old code, but if you're writing something new, it's pretty easy to use it like the Object.keys
pattern I -- and probably you if you're reading this -- was used to using. const myMap: Map<SomeType, SomeOtherType> = new Map()
, then later loop over it with myMap.forEach((val, key) => {... and TypeScript is happy here ...})
我最近碰到了这个问题,想再给堆上一个选项:转换为Map。为了支持旧代码而转换到Map是一个巨大的麻烦,但是如果你正在写一些新的东西,那么就很容易像我--如果你正在读这篇文章,可能你--习惯使用的Object.keys模式一样使用它。const myMap:Map = new Map(),然后稍后使用myMap.forEach((val,key)=> {. TypeScript在这里很开心...})
Object.fromEntries
has just the same issue 🤦
🤦也有同样的问题
+1 but it's just stupid, that we need this workaround. Object.keys
is already doing exactly that: "The Object.keys() method returns an array of a given object's own enumerable property names [...]" from MDN
+1但这太愚蠢了,我们需要这个变通办法。对象关键字已经在做这件事了:“对象关键字()方法返回给定对象自己的可枚举属性名称的数组[...]”来自MDN
我是一名优秀的程序员,十分优秀!