99
votes

When using Object.keys(obj), the return value is a string[], whereas I want a (keyof obj)[].

const v = {
    a: 1,
    b: 2
}

Object.keys(v).reduce((accumulator, current) => {
    accumulator.push(v[current]);
    return accumulator;
}, []);

I have the error:

Element implicitly has an 'any' type because type '{ a: number; b: number; }' has no index signature.

TypeScript 3.1 with strict: true. Playground: here, please check all checkboxes in Options to activate strict: true.

7
Don't think you can do better then a type assertion (Object.keys(v) as Array<keyof typeof v>) the definition is what it isTitian Cernicova-Dragomir

7 Answers

107
votes

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])[]);
37
votes

Based on Titian Cernicova-Dragomir answer and comment

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.

Discussions

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.

6
votes

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[]

5
votes

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;

Note:

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...

2
votes

You can use the Extract utility type to conform your param to only the keys of obj which are strings (thus, ignoring any numbers/symbols when you are coding).

const obj = {
  a: 'hello',
  b: 'world',
  1: 123 // 100% valid
} // if this was the literal code, you should add ` as const` assertion here

// util
type StringKeys<objType extends {}> = Array<Extract<keyof objType, string>>

// typedObjKeys will be ['a', 'b', '1'] at runtime
// ...but it's type will be Array<'a' | 'b'>
const typedObjKeys = Object.keys(obj) as StringKeys<typeof obj>

typedObjKeys.forEach((key) => {
  // key's type: 'a' | 'b'
  // runtime: 'a', 'b', AND '1'
  const value = obj[key]
  // value will be typed as just `string` when it's really `string | number`
})

All that said, most developers would probably consider having numbers as keys a poor design decision/bug to be fixed.

2
votes

Import approach

If you may be working in a monorepo, or are taking a 10-year perspective, you might prefer an import based approach (importing is pretty easy inside vscode anyway, literally just hit <tab>)

Just put this somewhere and then type ObjectT hit <tab> and it should get auto-imported when using vscode

type KeyOf<T> = Extract<keyof T, string>
type ValueOf<T> = T[KeyOf<T>]

/**
 * Nicely typed aliases for some `Object` Methods
 * - PSA: Don't mutate `yourObject`s
 * - Numerical keys are BAD `{ 1: 'ha!' }` may not appear in your resulting types
 * - Discussion: https://stackoverflow.com/a/65117465/565877
 */
export const ObjectTyped = {
  /**
   * Object.keys, but with nice typing (`Array<keyof T>`)
   */
  keys: Object.keys as <T extends {}>(yourObject: T) => Array<KeyOf<T>>,
  /**
   * Object.values, but with nice typing
   */
  values: Object.values as <T extends {}>(yourObject: T) => Array<ValueOf<T>>,
  /**
   * Object.entries, but with nice typing
   */
  entries: Object.entries as <T extends {}>(yourObject: T) => Array<[KeyOf<T>, ValueOf<T>]>,
  /**
   * Object.fromEntries, but with nice typing
   */
  fromEntries: Object.fromEntries as <K extends string, V>(
    yourObjectEntries: Array<[K, V]>
  ) => Record<K, V>,
}

Globals

If you prefer globals, add this to you src/index.ts:

[ Sorry removed code sample for simplicity+ease of maintenance. Don't use globals. Or, see answer history. ]

Here's what you'll see when hovering over TypedObject (Renamed to ObjectTyped since it's easier to discover. If you type Object then you can potentially notice ObjectTyped)

TypedObject info hover box

And .entries:

dot entries info hover box

And yourEntries:

yourEntries info hover box

Both approaches avoid mutating Object.keys behavior in any way across your whole codebase. It's an explicit opt-in. The javascript defining the aliases is right next to the TS declaration.

Of course, developers will need to discover ObjectTyped.keys, but, if it's used all over a codebase, it shouldn't take long to discover it.

======

Both approaches eschew using Extract<keyof T, string>, since these are meant to be used across a whole codebase, it might not be the safest default (You might want to know if you have non-string keys, and handle that on a case by case basis, or address the root cause of non-string keys)

I think this is un-necessary precaution that just adds complexity. See comments below BenCarp's answer. Just, don't write this odd code:

{
  1: 'asdf'
}

Due to Extract<keyof T, string> typescript will hide this numerical key from you. But, if you know anything about javascript, you may know that Object.keys will convert 1 into a string "1". Ideally typescript just had better default typing for Object.keys but sometimes you can't have your cake and eat it to.

If you just want to always see string keys, you can change Array<keyof T> to Array<Extract<keyof T, string>>, and you should still be fine most of the time. Yeah, we're just doing this nice+obvious thing now.

-1
votes

As a possible solution, you can iterate using for..in over your object:

for (const key in myObject) {
  console.log(myObject[key].abc); // works, but `key` is still just `string`
}

While this, as you said, would not work:

for (const key of Object.keys(myObject)) {
  console.log(myObject[key].abc); // doesn't!
}