Description
π Search Terms
type guard, narrowing, control flow analysis, property, variable index, bracket notation
β Viability Checklist
- 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 isn't a request to add a new utility type: https://github.com/microsoft/TypeScript/wiki/No-New-Utility-Types
- This feature would agree with the rest of our Design Goals: https://github.com/Microsoft/TypeScript/wiki/TypeScript-Design-Goals
β 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!
}
}
}