Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Narrow object property when key is a variable #56389

Closed
6 tasks done
jcalz opened this issue Nov 14, 2023 · 2 comments Β· Fixed by #57847
Closed
6 tasks done

Narrow object property when key is a variable #56389

jcalz opened this issue Nov 14, 2023 · 2 comments Β· Fixed by #57847
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript

Comments

@jcalz
Copy link
Contributor

jcalz commented Nov 14, 2023

πŸ” 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!
    }
  }
}
@fatcerberus
Copy link

fatcerberus commented Nov 14, 2023

I saw the title of this issue, clicked, and was all ready to dupe it to #10530... then I saw who posted it πŸ˜„

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.

Indeed, the refocusing was long overdue, IMO. It's so confusing to have to explain that "yeah this is still a dupe of 10530 even though the OP there uses a literal key which actually works now and has for a long time". Especially since that makes this look even more like a bug than it already did.

@fatcerberus
Copy link

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

Huh, never knew about the second part.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript
Projects
None yet
3 participants