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

Different behavior for {} and unknown narrowed to {} #50531

Closed
guillaumebrunerie opened this issue Aug 30, 2022 · 8 comments · Fixed by #50610
Closed

Different behavior for {} and unknown narrowed to {} #50531

guillaumebrunerie opened this issue Aug 30, 2022 · 8 comments · Fixed by #50610
Assignees
Labels
Bug A bug in TypeScript Fix Available A PR has been opened for this issue

Comments

@guillaumebrunerie
Copy link

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 {}.

@fatcerberus
Copy link

fatcerberus commented Aug 31, 2022

I'm guessing what happens is that once y gets narrowed to {}, TS decides that "a" in y is impossible since in normally narrows unions via property existence and {} is an empty object type and thus gets eliminated, leaving never. That doesn't explain why x isn't also narrowed to never, 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
Copy link

For the record, the narrowing of y to {} is new - 4.7.4 leaves y as unknown and subsequently errors on "a" in y: Playground

@guillaumebrunerie
Copy link
Author

{} 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 to never.

@fatcerberus
Copy link

fatcerberus commented Sep 1, 2022

{} is the empty object type in the same sense that { a: string } is the “object with a single property called a 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 to object.

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 to never by the same check. What I don’t understand is why the first case doesn’t also narrow to never.

@guillaumebrunerie
Copy link
Author

Oh, I see what you mean. I didn’t realize that "a" in x narrows { b: string } | { c: string } to never, it's unsound but I guess also a very common pattern in JS.

@fatcerberus
Copy link

fatcerberus commented 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, because in isn't supposed to narrow non-union types IIRC.

@guillaumebrunerie
Copy link
Author

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.

@fatcerberus
Copy link

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.

This was my hypothesis as well.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Bug A bug in TypeScript Fix Available A PR has been opened for this issue
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants