microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
100.75k stars 12.46k forks source link

Inverted union type narrowing was broken in 4.9 #58861

Open Woodz opened 4 months ago

Woodz commented 4 months ago

🔎 Search Terms

"union type", "narrowing", "inverted", "4.9"

🕗 Version & Regression Information

⏯ Playground Link

Playground link

💻 Code

interface Foo { foo: string; }

interface Bar { bar: string; }

interface Baz { baz: string; }

function myFunction(input: Foo | Bar | Baz) {
    const isBaz = 'baz' in input;
    const isBar = 
      //'bar' in input; // This works to narrow `bar` when true and narrow `foo` in the else block
      !('foo' in input) && !isBaz; // This works to narrow `bar` when true but does not narrow `foo` in the else block
    let x: string;
    if (isBaz) {
      x = input.baz;
    } else if (isBar) {
      x = input.bar;
    } else {
      // In 4.8.4 and earlier, `input` is narrowed to `Foo`
      // In 4.9.5 and later, `input` is narrowed to `Foo | ((Foo | Bar) & Record<"baz", unknown>)`
      x = input.foo;
    }
}

🙁 Actual behavior

In 4.9.5 and later, in the else block, input.foo incorrectly narrows and errors

🙂 Expected behavior

In 4.8.4 and earlier, in the else block, input.foo correctly narrows

Additional information about the issue

No response

Andarist commented 4 months ago

bisects to https://github.com/microsoft/TypeScript/pull/50666

Andarist commented 4 months ago

This playground might also be helpful to see what is happening. The inverted narrowing works just fine:

function myFunction4(input: Foo | Bar | Baz) {
  if (!("baz" in input)) {
    input;
    // ^? (parameter) input: Foo | Bar
  } else {
    input;
    // ^? (parameter) input: Baz
  }
}

The issue here is specific to the fact that inverted narrowing is used on the type is already refined using the same non-inverted check.