Reputation: 220954
Title says it all - why doesn't Object.keys(x)
in TypeScript return the type Array<keyof typeof x>
? That's what Object.keys
does, so it seems like an obvious oversight on the part of the TypeScript definition file authors to not make the return type simply be keyof T
.
Should I log a bug on their GitHub repo, or just go ahead and send a PR to fix it for them?
Upvotes: 224
Views: 84012
Reputation: 959
This is what I did without breaking anything:
declare global {
interface ObjectConstructor {
typedKeys<T>(o: T): (keyof T)[]
}
}
Object.typedKeys = Object.keys
Upvotes: 0
Reputation: 111
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:
/**
* 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.
Upvotes: 1
Reputation: 109
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:
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
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)[]
}
}
Upvotes: -1
Reputation: 220954
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
?
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.
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.
Basically, (at least) one of the following four things can't be true:
keyof T
is an exhaustive list of the keys of T
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"
.
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.
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.
Commentors have implied that Object.keys
could safely return keyof T
if the argument was a generic value. This is still wrong. Consider:
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);
Upvotes: 251
Reputation: 10928
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.
Upvotes: 19
Reputation: 31
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));
Upvotes: 3
Reputation: 6039
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:
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;
}
Upvotes: 45