Skip to content

Incorrect post-narrowing merge of typeof symbol | {} | (() => void)Β #45467

Description

Bug Report

πŸ”Ž Search Terms

switch incorrectly narrows type

πŸ•— Version & Regression Information

v4.3.5

⏯ Playground Link

Playground link with relevant code

πŸ’» Code

const symbol = Symbol("Test");

function f(x: 1 | "a" | typeof symbol | {} | (() => void)): void {
  switch (typeof x) {
    case "number":
    case "object":
    case "string":
    case "symbol":
  }

  switch (typeof x) {
    case "function":
    case "object":
    case "string":
    case "symbol":
  }

  switch (typeof x) {
    case "function":
    case "number":
    case "string":
    case "symbol":
  }

  switch (typeof x) {
    case "function":
    case "number":
    case "object":
    case "symbol":
  }

  switch (typeof x) {
    case "function":
    case "number":
    case "object":
    case "string":
  }

  switch (typeof x) {
    case "function":
    case "number":
    case "object":
    case "string":
    case "symbol":
  }
}

πŸ™ Actual behavior

Starting from 4th switch the type of x is narrowed to {}.

Screen

πŸ™‚ Expected behavior

The type of x should not be narrowed

Activity

  1. IllusionMH commented on Aug 16, 2021

    @IllusionMH
    Contributor

    More like it gave up on keeping union and used {} as common type ("widened"?).

    Because everything from that union is assignable to {}

    const symbol = Symbol("Test");
    
    // no errors
    const x1: 1 | "a" | typeof symbol | {} | (() => void) = 123;
    const x2: 1 | "a" | typeof symbol | {} | (() => void) = "not a";
    const x3: 1 | "a" | typeof symbol | {} | (() => void) = Symbol("not Test");
    const x4: 1 | "a" | typeof symbol | {} | (() => void) = (a: number) => a;
    
    // no errors
    const y1: {} = 1;
    const y2: {} = "a";
    const y3: {} = Symbol("a");
    const y4: {} = () => {};
    
    // errors as expected
    const z1: 1 | "a" | typeof symbol | (() => void) = 123;
    const z2: 1 | "a" | typeof symbol | (() => void) = "not a";
    const z3: 1 | "a" | typeof symbol | (() => void) = Symbol("not Test");
    const z4: 1 | "a" | typeof symbol | (() => void) = (a: number) => a;

    Playground

    Not sure about your use case but object instead of {} might be closer to what you are looking for.

  2. RyanCavanaugh commented on Aug 16, 2021

    @RyanCavanaugh
    Member

    Simpler repro

    const symbol = Symbol("Test");
    
    function f(x: typeof symbol | {} | (() => void)): void {
      if (typeof x === "function") { }
      x;
    }

    These issues happen when the re-unioning logic gets messed up when merging the two incoming control flow edges. But there's not much reason to write this code in first place.

  3. changed the title [-]Switch incorrectly narrows type[/-] [+]Incorrect post-narrowing merge of `typeof symbol | {} | (() => void)`[/+] on Aug 16, 2021
  4. fatcerberus commented on Aug 18, 2021

    @fatcerberus

    This looks like subtype reduction, similar to what happens in #39434.

  5. Andarist commented on Aug 13, 2023

    @Andarist
    Contributor

    Ryan Cavanaugh (@RyanCavanaugh) Jake Bailey (@jakebailey) should this still be classified as a bug? the weird behavior with multiple switch cases from the OP was already "fixed" by #49119 . As Ryan's answer suggests - this is just union reduction at play when merging control flow paths. I don't exactly see this particular example to be a good motivation for reconsidering the used algorithm.

  6. RyanCavanaugh commented on Aug 16, 2023

    @RyanCavanaugh
    Member

    I think ideally we would still go back to the declared type -- you could imagine an algorithm that detects that all incoming paths have an uninterrupted flow from the origin and does this -- but yeah, this isn't wrong per our normal stance of "subtype reduction can occur at any time".

  7. locked as resolved and limited conversation to collaborators on Oct 21, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    BugA bug in TypeScript

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions