Skip to content

Infer keyof Obj instead of string in for(key in obj) loops and Object.keys()Β #44706

@antoinep92

Description

@antoinep92

Suggestion

πŸ” Search Terms

for in loop
string keyof infer

βœ… Viability Checklist

My suggestion meets these guidelines:

  • This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • This wouldn't change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.

⭐ Suggestion

Object.keys(obj as Obj) should have static type Array<keyof Obj> instead of string[]

in for(key in obj as Obj), key should have static type keyof Obj instead of string

Even when objects have well known keys, it is a common practice in javascript to iterate dynamically over the keys using Object.keys or for..in loops.

In typescript it is a bit convoluted to achieve, which confuses newcomers and is a bit painful because:

  1. Object.keys()always returns string[] and in for(key in object), key is always inferred as string
  2. object[key] only work with strings if we explicitly declared a string index in the type declaration of object

πŸ“ƒ Motivating Example

interface Conf {
  color?: string
  size?: number
}
const defaultConf: Conf = { color: "red", size: 13 }
function init(conf?: Conf) {
  if(!conf) conf = {};
  for(const k in defaultConf) {
    if(conf[k] === undefined) conf[k] = defaultConf[k];
// Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'Conf'.
//  No index signature with a parameter of type 'string' was found on type 'Conf'.(7053)
  }
}

This code is sound, in the sense that it wouldn't generate runtime errors or violate (implicit) code invariants and hypothesis. But it doesn't compiles in typescript. To make matter worse, when reading the error, one could be tempted to change the Conf interface to:

interface Conf {
  color?: string
  size?: number
  [key: string]: any
}

And indeed, the code then compiles. Unfortunately, doing so just made the code less type-safe, by broadening the Conf interface too much for no real reason. I think this is related to this issue about the error wording.

The better solution, would be to change the offending line with:

if(conf[k as keyof Conf] === undefined) conf[k as keyof Conf] = defaultConf[k as keyof Conf];

This compiles, and does what we want. But I find it a bit verbose, and generaly I'm not a fan of casts.

I tried changing the type at the for loop instead, but no luck:

  for(const k: keyof Conf in defaultConf) {
// The left-hand side of a 'for...in' statement cannot use a type annotation.(2404)

This is a bit frustrating because for me this is kind of the definition of keyof so it's unfortunate.

Maybe I missed something, an there are probably some corner-case like Record<number, any> where keyof would not be backward compatible with string. But for the most part, I think it shouldn't break anything. Corner cases could be handled using a more complex definition:

  • If keyof is compatible with string, then use keyof
  • Otherwise, use string

πŸ’» Use Cases

There are many use-cases. Javascript (and by extension typescript) is very dynamic, and there are many situations when it makes sense to iterate on objects keys instead of duplicating code: generic checks, deep copies, deep transformations, mixins, and many more.

I feel like there is a broader issue at play, where this kind of small grains of sand add up, and kind of push away developers away from some coding patterns in favor of object oriented paradigm. But hopefully I'm mistaken, and this is probably not the place for this discussion.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions