keys() returns an array whose elements are strings corresponding to the enumerable properties found directly upon object. The ordering of the properties is the same as that given by looping over the properties of the object manually. You get an array of strings, because Property names are strings by definition.
Object.keys
returns a string[]
. This is by design as described in this issue
This is intentional. Types in TS are open ended. So keysof will likely be less than all properties you would get at runtime.
There are several solution, the simplest one is to just use a type assertion:
const v = {
a: 1,
b: 2
};
var values = (Object.keys(v) as Array<keyof typeof v>).reduce((accumulator, current) => {
accumulator.push(v[current]);
return accumulator;
}, [] as (typeof v[keyof typeof v])[]);
You can also create an alias for keys
in Object
that will return the type you want:
export const v = {
a: 1,
b: 2
};
declare global {
interface ObjectConstructor {
typedKeys<T>(obj: T): Array<keyof T>
}
}
Object.typedKeys = Object.keys as any
var values = Object.typedKeys(v).reduce((accumulator, current) => {
accumulator.push(v[current]);
return accumulator;
}, [] as (typeof v[keyof typeof v])[]);
Use type assertion only if you know that your object doesn't have extra properties (such is the case for an object literal but not an object parameter).
Explicit assertion
Object.keys(obj) as Array<keyof typeof obj>
Hidden assertion
const getKeys = Object.keys as <T extends object>(obj: T) => Array<keyof T>
Use getKeys
instead of Object.keys
. getKeys
is a ref to Object.keys
, but the return is typed literally.
One of TypeScript’s core principles is that type checking focuses on the shape that values have. (reference)
interface SimpleObject {
a: string
b: string
}
const x = {
a: "article",
b: "bridge",
c: "Camel"
}
x
qualifies as a SimpleObject
because it has it's shape. This means that when we see a SimpleObject
, we know that it has properties a
and b
, but it might have additional properties as well.
const someFunction = (obj: SimpleObject) => {
Object.keys(obj).forEach((k)=>{
....
})
}
someFunction(x)
Let's see what would happen if by default we would type Object.keys as desired by the OP "literally":
We would get that typeof k
is "a"|"b"
. When iterating the actual values would be a
, b
, c
. Typescript protects us from such an error by typing k as a string.
Type assertion is exactly for such cases - when the programmer has additional knowledge. if you know that obj
doesn't have extra properties you can use literal type assertion.
See https://github.com/microsoft/TypeScript/issues/20503.
declare const BetterObject: {
keys<T extends {}>(object: T): (keyof T)[]
}
const icons: IconName[] = BetterObject.keys(IconMap)
Will retain type of keys instead of string[]
I completely disagree with Typescript's team's decision...
Following their logic, Object.values
should always return any, as we could add more properties at run-time...
I think the proper way to go is to create interfaces with optional properties and set (or not) those properties as you go...
So I simply overwrote locally the ObjectConstructor
interface, by adding a declaration file (aka: whatever.d.ts) to my project with the following content:
declare interface ObjectConstructor extends Omit<ObjectConstructor, 'keys' | 'entries'> {
/**
* Returns the names of the enumerable string properties and methods of an object.
* @param obj 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 extends any[]>(obj: O): Array<keyof O>;
keys<O extends Record<Readonly<string>, any>>(obj: O): Array<keyof O>;
keys(obj: object): string[];
/**
* Returns an array of key/values of the enumerable properties of an object
* @param obj Object that contains the properties and methods. This can be an object that you created or an existing Document Object Model (DOM) object.
*/
entries<T extends { [K: Readonly<string>]: any }>(obj: T): Array<[keyof T, T[keyof T]]>
entries<T extends object>(obj: { [s: string]: T } | ArrayLike<T>): [string, T[keyof T]][];
entries<T>(obj: { [s: string]: T } | ArrayLike<T>): [string, T][];
entries(obj: {}): [string, any][];
}
declare var Object: ObjectConstructor;
Object.keys/Object.entries of primitive types (object) will return never[] and [never, never][] instead of the normal string[] and [string, any][]. If anyone knows a solutions, please, feel free to tell me in the comments and I will edit my answer
const a: {} = {};
const b: object = {};
const c: {x:string, y:number} = { x: '', y: 2 };
// before
Object.keys(a) // string[]
Object.keys(b) // string[]
Object.keys(c) // string[]
Object.entries(a) // [string, unknown][]
Object.entries(b) // [string, any][]
Object.entries(c) // [string, string|number][]
// after
Object.keys(a) // never[]
Object.keys(b) // never[]
Object.keys(c) // ('x'|'y')[]
Object.entries(a) // [never, never][]
Object.entries(b) // [never, never][]
Object.entries(c) // ['x'|'y', string|number][]
So, use this with caution...
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With