Mesrai
Back to blog
// essayTechnical Deep Dive

Object.keys Type Widening: Mesrai's as const Suggestion

Real PR catch: Object.keys then index access — implicit any error. Object.entries + typed-helper fixes.

Mesrai TeamAugust 14, 20268 min read

An audit loop walking config fields with `Object.keys`. Each iteration tries to read `config[k]` but `k` is `string`, not `keyof Config`. Index access errors out under strict mode; under loose mode, downstream type inference degrades.

The vulnerable diff

tslib/audit-config.ts
// lib/audit-config.ts
interface Config { host: string; port: number; useTLS: boolean }

function audit(config: Config) {
  // BUG: keys typed as string, index access via string returns any
  Object.keys(config).forEach(k => {
    check(config[k]);  // error: Element implicitly has an 'any' type
  });
}

What is wrong

`Object.keys()` has return type `string[]` by design — at runtime an object can have additional keys beyond what the type system knows about. This makes type-safe iteration awkward. The common patterns are: cast the result to `(keyof T)[]` (asserting no extra keys), use a `for...of` over `Object.entries`, or use a typed helper. Each has trade-offs.

The attack

Compile error under strict mode:

textexploit
error TS7053: Element implicitly has an 'any' type because
expression of type 'string' can't be used to index type 'Config'.

Common workarounds spread `any` downstream, weakening type safety further.

Mesrai's review comment

textreview-comment.txt
mesraipilot · Bot · reviewed 20 sec ago

[mesrai] [code-review] [Language] [Type-Widening] [low]

`Object.keys(config)` returns string[] not (keyof Config)[].

Three options:

  // Option 1: cast (assert no extra keys)
  (Object.keys(config) as (keyof typeof config)[]).forEach(k => {
    check(config[k]);
  });

  // Option 2: Object.entries — types both key and value better
  Object.entries(config).forEach(([k, v]) => {
    // k: string, v: Config[keyof Config] = string | number | boolean
    check(v);
  });

  // Option 3: typed helper
  function typedKeys<T extends object>(o: T): (keyof T)[] {
    return Object.keys(o) as (keyof T)[];
  }
  typedKeys(config).forEach(k => check(config[k]));

Prefer Object.entries when you need both key and value — it types
the value via T[keyof T] which is usually what you want.

The fix

tslib/audit-config.ts (fixed)
// lib/audit-config.ts — fixed
function audit(config: Config) {
  Object.entries(config).forEach(([k, v]) => {
    check(v);
  });
}

`Object.entries` types the value as a union of all the field types. No cast needed in many cases.

Why human review missed it

Object.keys widening is a TS-vs-JS design tension. Many devs solve it with broad casts that spread `any`. Mesrai flags every `Object.keys(typed).forEach(k => obj[k])` shape and recommends `Object.entries` or a typed helper.

Related rules + further reading

Mesrai rule pack: language/object-keys-widening — flags Object.keys + index access without typed-keys cast.

TypeScript GitHub: long-standing discussion on Object.keys typing.

Common in config / state-walking code.

Takeaway

Object.entries types both sides. Or typedKeys helper. Avoid raw Object.keys + index access on typed objects.

// try

See it on your next PR.

Free for individuals. Install in two minutes. Mesrai reviews every commit.