Recipe · Objects and data

Why does Object.keys return string[], and how do I keep the key type?

  • Node.js (ES modules)
  • Object.keys
  • keyof
  • Type casts
  • Checked with TypeScript 5.9

The problem I loop over Object.keys(stock) and then read stock[item], and tsc says the element implicitly has an 'any' type. The keys come from the object itself. Why are they plain strings?

The short answer

Name the key type with keyof typeof, and cast the result of Object.keys to an array of it. When you only need the values alongside the keys, Object.entries already types them:

stock.js stock.ts TypeScript 5.9.3 TypeScript 5.9.3, run 21 October 2026: No errors. 6.0.3 TypeScript 6.0.3, run 21 October 2026: the same as 5.9.3. No errors. 7.0.2 TypeScript 7.0.2, run 21 October 2026: the same as 5.9.3. No errors. checked 21 October 2026
const stock = { lamp: 4, desk: 0, chair: 12 };

/** @typedef {keyof typeof stock} Item */

for (const item of /** @type {Item[]} */ (Object.keys(stock))) {
  console.log(item, stock[item]);
}

for (const [item, count] of Object.entries(stock)) {
  console.log(item.toUpperCase(), count.toFixed(0));
}

/**
 * @template {object} T
 * @param {T} object
 * @returns {(keyof T)[]}
 */
export function keysOf(object) {
  return /** @type {(keyof T)[]} */ (Object.keys(object));
}

const empty = keysOf(stock).filter((item) => stock[item] === 0);
console.log(empty);
const stock = { lamp: 4, desk: 0, chair: 12 };

type Item = keyof typeof stock;

for (const item of Object.keys(stock) as Item[]) {
  console.log(item, stock[item]);
}

for (const [item, count] of Object.entries(stock)) {
  console.log(item.toUpperCase(), count.toFixed(0));
}

export function keysOf<T extends object>(object: T): (keyof T)[] {
  return Object.keys(object) as (keyof T)[];
}

const empty = keysOf(stock).filter((item) => stock[item] === 0);
console.log(empty);
Typed keys from Object.keys with a cast, and entries that keep the value type.
jsconfig.json for this example
{
  "compilerOptions": {
    "checkJs": true,
    "noEmit": true,
    "strict": true,
    "target": "es2022",
    "module": "nodenext",
    "types": []
  },
  "include": ["*.js"]
}

The keysOf helper puts the cast in one place, so the loops that call it need no JSDoc at all.

Why it works

A JavaScript object can hold more properties than its type mentions: an object of a wider type can be passed where a narrower one is expected. So TypeScript’s library declares Object.keys(o) as returning string[], the only promise it can keep. When you know the object has no extra keys, the cast /** @type {Item[]} */ (Object.keys(stock)) says so. The parentheses are part of the JSDoc cast syntax; without them the comment is just a comment.

Object.entries(stock) returns [string, number][]: the keys are still strings, but the values keep their type, because every value of stock is a number.

The same thing in TypeScript

The TypeScript equivalent tab writes Object.keys(stock) as Item[] and (keyof T)[] in the helper’s signature. A cast is the same promise in both languages; JavaScript spells it with a JSDoc comment and parentheses.

What goes wrong

Without the cast, item is a string, and a string cannot index an object whose keys are lamp, desk and chair (TS7053):

stock.js Error confirmed 5.9.3 TypeScript 5.9.3, run 21 October 2026: stock.js(5,21): error TS7053: Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{ lamp: number; desk: number; chair: number; }'. No index signature with a parameter of type 'string' was found on type '{ lamp: number; desk: number; chair: number; }'. 6.0.3 TypeScript 6.0.3, run 21 October 2026: the same as 5.9.3. stock.js(5,21): error TS7053: Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{ lamp: number; desk: number; chair: number; }'. No index signature with a parameter of type 'string' was found on type '{ lamp: number; desk: number; chair: number; }'. 7.0.2 TypeScript 7.0.2, run 21 October 2026: the same as 5.9.3. stock.js(5,21): error TS7053: Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{ lamp: number; desk: number; chair: number; }'. No index signature with a parameter of type 'string' was found on type '{ lamp: number; desk: number; chair: number; }'. checked 21 October 2026
const stock = { lamp: 4, desk: 0, chair: 12 };

for (const item of Object.keys(stock)) {
  console.log(item, stock[item]);error TS7053: Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{ lamp: number; desk: number; chair: number; }'.
  No index signature with a parameter of type 'string' was found on type '{ lamp: number; desk: number; chair: number; }'.
}
Object.keys gives strings, which cannot index the object.
jsconfig.json for this example
{
  "compilerOptions": {
    "checkJs": true,
    "noEmit": true,
    "strict": true,
    "target": "es2022",
    "module": "nodenext",
    "types": []
  },
  "include": ["*.js"]
}

Gotchas

  • in does not narrow the key. if (item in stock) stock[item] still fails; a type guard such as Object.hasOwn inside a function that returns item is Item does narrow it.
  • A Record with string keys needs no cast. For an object typed Record<string, number> any string is a valid key already; the cast matters for objects with a fixed set of keys.
  • Casts are not checks. A cast to the wrong key type is accepted as long as the types overlap; prefer the helper, which ties the key type to the object it was called with.

Questions

Is the cast to the key type safe?

It is safe when the object really has only the keys its type lists, as with an object literal you wrote yourself. An object that came from elsewhere may carry extra keys at runtime, which is exactly why TypeScript types the keys as string.

Can I use for...in instead?

The loop variable of for...in is also a string, for the same reason, so stock[item] fails the same way. Cast it, or loop over the typed keys from keysOf.

Sources

Last reviewed 21 October 2026.