This is essentially the same issue as microsoft/TypeScript#10530; type narrowing from control flow analysis only happens for properties that are directly literals like "b"
and not for arbitrary expressions whose types are literal types. Issue #10530 talks about narrowing via property access... like a.b
or a["b"]
, which does cause a
to be narrowed, vs a[key]
, which does not.
As you've noticed, this also happens with the in
operator type guard (as implemented in microsoft/TypeScript#15256), where "b" in a
narrows the type of a
, but key in a
does not. This is not explicitly mentioned in #10530 (which pre-dates the in
type guard) but I don't think there's another existing issue specifically about this.
According to microsoft/TypeScript#10565, an initial attempt to address the issue in #10530, adding type guard functionality for arbitrary expressions of literal types significantly worsens the compiler performance. Maybe performing extra analysis for all x in y
checks would be less expensive than performing extra analysis for all y[x]
property accesses, but at least so far nobody has cared much.
You could always open your own issue about it in GitHub (many issues end up being duplicates, and I'm not 100% sure this wouldn't just be considered a duplicate of #10530, or that there isn't some other issue this duplicates), but practically speaking it's probably not going to change anytime soon.
If you want a workaround for the case where you can't just replace key
with a string literal, you could write your own user-defined type guard function called hasProp(obj, prop)
. The implementation would just return prop in obj
, but its type signature explicitly says that a true
result should cause obj
to be narrowed to just those union members with a key of type prop
:
function hasProp<T extends object, K extends PropertyKey>(
obj: T, prop: K
): obj is Extract<T, { [P in K]?: any }> {
return prop in obj;
}
and then in your function, replace key in a
with hasProp(a, key)
:
function f3(a: A) {
const key = 'b';
if (hasProp(a, key)) {
return a[key]; // okay
}
return 42;
}
Playground link to code
key
type. For example,const key = 'b' as keyof A;
. – Pectenkeyof A
isnever
because there are no keys common to all the constituents of theA
union. Writingconst key = 'b' as keyof A
is equivalent to writingconst key = 'b' as never
which is explicit but unsound and usually not advisable. The type inferred byconst key = 'b'
is just'b'
. If you are explicit about that, as inconst key: 'b' = 'b'
or maybe `const key = 'b' as 'b', you have the same as shown above. The problem is not explicitness. – Nestornestorian