Closed
Description
Bug Report
I found a curious situation in Typescript 4.8 where a variable of type unknown
narrowed down to {}
behaves differently than a variable of type {}
from the beginning.
🔎 Search Terms
4.8, narrow, in
Not sure if it is related to #50527.
🕗 Version & Regression Information
- I was unable to test this on prior versions because it relies on a new 4.8 feature.
⏯ Playground Link
Playground link with relevant code
💻 Code
const f = (x: {}, y: unknown) => {
if (!("a" in x)) {
return;
}
console.log(x);
// x stays {} (is not narrowed), not optimal but at least not wrong
if (!y) {
return;
}
// y is narrowed to {}
if (!("a" in y)) {
return;
}
console.log(y);
// y is narrowed to never, which is clearly incorrect, and pretty strange because
// it had the same type as x before and we run the same guard
}
🙁 Actual behavior
Narrowing unknown
to {}
and then using the in
operator results in an incorrect type, whereas it doesn’t happen when starting with a variable of type {}
.
🙂 Expected behavior
Narrowing should behave the same way whether we start with the type {}
or narrow unknown
down to {}
.
Activity
fatcerberus commentedon Aug 31, 2022
I'm guessing what happens is that once
y
gets narrowed to{}
, TS decides that"a" in y
is impossible sincein
normally narrows unions via property existence and{}
is an empty object type and thus gets eliminated, leavingnever
. That doesn't explain whyx
isn't also narrowed tonever
, though. Quite odd indeed.I think there's an internal distinction between regular
{}
and "fresh{}
" that I don't fully understand that might be responsible for this. @ahejlsberg might know what's going on.fatcerberus commentedon Aug 31, 2022
For the record, the narrowing of
y
to{}
is new - 4.7.4 leavesy
asunknown
and subsequently errors on"a" in y
: Playgroundguillaumebrunerie commentedon Sep 1, 2022
{}
isn't the empty object type, it's the "anything not null or undefined" type. So"a" in y
should ideally narrow{}
to{a: unknown}
(feature request #21732), but there is in any case no reason to narrow it tonever
.fatcerberus commentedon Sep 1, 2022
{}
is the empty object type in the same sense that{ a: string }
is the “object with a single property calleda
which is a string” type. Structural typing may let you assign other things to it (like primitives) but it’s still really an object type. It’s even assignable toobject
.Case in point: Primitives can also be assigned to
{ toString(): string }
, so{}
isn’t really special in that regard. It’s just an accident of structural typing.For the narrowing to
never
, like I said above I think it happens for the same reason that{ b: string } | { c: string }
is narrowed tonever
by the same check. What I don’t understand is why the first case doesn’t also narrow tonever
.guillaumebrunerie commentedon Sep 1, 2022
Oh, I see what you mean. I didn’t realize that
"a" in x
narrows{ b: string } | { c: string }
tonever
, it's unsound but I guess also a very common pattern in JS.fatcerberus commentedon Sep 1, 2022
Yeah, every once in a while someone opens an issue about
in
-based narrowing being unsound that then gets closed as by design because it's such a common JS pattern. I'm pretty sure this is a bona fide bug though, becausein
isn't supposed to narrow non-union types IIRC.guillaumebrunerie commentedon Sep 1, 2022
Oh, maybe the narrowing of
unknown
to{}
actually narrows it internally to a union type with only one component (| {}
if you see what I mean). That could explain it I guess.{}
in strict equality condition since 4.8 #50567fatcerberus commentedon Sep 1, 2022
This was my hypothesis as well.
in
operator shouldn't narrow{}
originating inunknown
#50610T & object
narrows tonever
#50639