Skip to content

Narrow object property when key is a variableΒ #56389

Closed
@jcalz

Description

@jcalz

πŸ” Search Terms

type guard, narrowing, control flow analysis, property, variable index, bracket notation

βœ… Viability Checklist

⭐ Suggestion

This is a re-opening or re-focusing of #10530. The original motivating example (narrowing obj via checking obj["key"] the same way as obj.key when "key" is the discriminant property) was fixed a long time ago, but the issue stayed open and started collecting requests for a more general feature of narrowing object properties by bracket access. Other issues about the general feature were closed as duplicates. But now #10530 is closed, and any unmet needs from there should be moved to a new issue. Like this one, maybe:

Please enable narrowing of object properties accessed via bracket notation based on the identity of the key, not just its type. If guard() is a type guard, then if guard(obj[key]) { β‹― obj[key] β‹― } should serve to narrow obj[key] inside the block, no matter what the type of key is.

πŸ“ƒ Motivating Example

Currently narrowing obj[key] only works if key is actually a string literal or a const of a single string literal type:

namespace Good {
  declare const obj: { [key: string]: string | undefined };
  if (obj["a"]) { obj["a"].toUpperCase() }; // okay  
  declare const key: "a";
  if (obj[key]) { obj[key].toUpperCase() }; // okay    
}

But superficially similar constructions do not work, where key is not const, and where the type is a wide type like string, or a union type like "a" | "b", or a generic type like K extends string:

namespace Problem1 {
  declare const obj: { [key: string]: string | undefined };
  declare let key: "a";
  if (obj[key]) { obj[key].toUpperCase() } // error! possibly undefined
}

namespace Problem2 {
  declare const obj: { [key: string]: string | undefined };
  declare const key: string;
  if (obj[key]) { obj[key].toUpperCase() } // error! possibly undefined
}

namespace Problem3 {
  declare const obj: { a?: string, b?: string };
  declare const key: "a" | "b";
  if (obj[key]) { obj[key].toUpperCase() } // error! possibly undefined
}

namespace Problem4 {
  function f<K extends string>(obj: { [P in K]?: string }, k: K) {
    const key: K = k;
    if (obj[key]) { obj[key].toUpperCase() } // error! possibly undefined
  }
}

It would be great if all of those would "just work" (although I can understand why it's not trivial to do so, or that one can imagine some cases where it would be a bad idea to allow it).

πŸ’» Use Cases

See #10530 and various issues closed as duplicates for more use cases.

The standard workaround is to assign the property to a new const and do the guarding on that:

namespace Workaround {
  declare const obj: { [key: string]: string | undefined };
  declare let key: "a";
  const ok = obj[key];
  if (ok) { ok.toUpperCase() } // okay
}

This works well, although people on Stack Overflow often express dissatisfaction with that suggestion (e.g., "why should I have to create a new variable just to appease TypeScript?").

Unfortunately some folks also expect to be able to assign something to the narrowed property, and there's just no great way to do that without type assertions:

namespace Ugh {
  function f(
    obj: { a: string, b: number, c: string, d: number, e: string, f: number },
    key: keyof typeof obj
  ) {
    if (typeof obj[key] === "string") {
      obj[key] = obj[key].toUpperCase() // error!
    } else {
      obj[key] = obj[key] + 1; // error!
    }
  }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    Awaiting More FeedbackThis means we'd like to hear from more people who would be helped by this featureSuggestionAn idea for TypeScript

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions