Skip to content

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

Closed
@guillaumebrunerie

Description

@guillaumebrunerie

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

fatcerberus commented on Aug 31, 2022

@fatcerberus

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

fatcerberus commented on Aug 31, 2022

@fatcerberus

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

guillaumebrunerie commented on Sep 1, 2022

@guillaumebrunerie
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

fatcerberus commented on Sep 1, 2022

@fatcerberus

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

guillaumebrunerie commented on Sep 1, 2022

@guillaumebrunerie
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

fatcerberus commented on Sep 1, 2022

@fatcerberus

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

guillaumebrunerie commented on Sep 1, 2022

@guillaumebrunerie
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.

added this to the TypeScript 4.8.3 milestone on Sep 1, 2022
fatcerberus

fatcerberus commented on Sep 1, 2022

@fatcerberus

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

Metadata

Metadata

Assignees

Labels

BugA bug in TypeScriptFix AvailableA PR has been opened for this issue

Type

No type

Projects

No projects

Relationships

None yet

    Development

    Participants

    @guillaumebrunerie@andrewbranch@fatcerberus@ahejlsberg@typescript-bot

    Issue actions

      Different behavior for {} and unknown narrowed to {} · Issue #50531 · microsoft/TypeScript