Skip to content

Composite types fails to type narrowing when it contains two or more possibilities to a narrowable propertyΒ #56023

Closed

Description

πŸ”Ž Search Terms

narrowing two possibilities type

πŸ•— Version & Regression Information

This is the behavior in every version 5.2.2, 4.9.5, 3.9.7, and I reviewed the FAQ for entries about type narrowing issues.

⏯ Playground Link

https://www.typescriptlang.org/play?target=9&jsx=4#code/KYDwDg9gTgLgBDAnmYcCiICGBbMAbYAFWVQF4AoOOAHzgG9KqmxMocBBALjgCJMeavAEY8A3IyZwWbbACFuAOwCu2IcCjjJVaTgDCilWo0S4AX0a06U1h248AxgNo8AJmOsz5cZavVnx5ABmSgr2MACWEApwUCEYOPjAABRWoAkEZtypWLgE3PG5RCRmAJT0jOGBcEkmaYUAdDrY7HCkbcJO1LU5iY02za3tDp3d6cB9Mi1tpLxujGUMklDAMEpQ0XW9TbKaZuSMy6vrcJsEE3ri5kA

πŸ’» Code

// Type definition
export type ExampleType =
  | {
      paramA: "a" | "b";
      paramB: number;
      paramC: number;
    }
  | { paramA: "c" | "d"; paramB: number };

// Function with type narrowing issue
function runExample({ example }: { example: ExampleType }) {
  if (
    example.paramA === "b" ||
    example.paramA === "c" ||
    example.paramA === "d"
  ) {
    return example.paramB;
  }

  return example.paramC; // Type narrowing issue here
}

πŸ™ Actual behavior

The TypeScript type narrowing is not working as expected. Even when the example.paramA is narrowed down to "c" or "d" inside the if statement, TypeScript still considers it a union of all possible types, resulting in a type error when trying to access example.paramC.

πŸ™‚ Expected behavior

I expected TypeScript to correctly narrow down the type of example inside the if statement, so that accessing example.paramC would not result in a type error when example.paramA not is "b", "c" or "d".

Additional information about the issue

Let's imagine that I have a TS type that can represent two data types. The first one allows paramA to be "a" or "b", and in this case, it has two additional parameters, paramB and paramC. The other type also has paramA, but it can be "c" or "d". However, in these latter two cases, it only has an additional parameter paramB. In other words, paramC only exists when paramA is exclusively "a" or "b".

So, I perform type narrowing using if() statements that check if paramA is "b", "c", or "d", then it will return the value of paramB. Otherwise, it is understood that paramA is "a" since it is the only option available, so we return paramC.

Here is my ExampleType type signature:

export type ExampleType =
  | {
      paramA: "a" | "b";
      paramB: number;
      paramC: number;
    }
  | { paramA: "c" | "d"; paramB: number };

Note that this rule matches the requirement mentioned earlier. So, only when paramA is "a" or "b", we can access paramB and paramC, and when paramA is "c" or "d", we have only one parameter paramB, and paramC is not available.

Therefore, we can understand that the following construction would be valid:

function runExample({ example }: { example: ExampleType }) {
  if (
    example.paramA === "b" ||
    example.paramA === "c" ||
    example.paramA === "d"
  ) {
    return example.paramB;
  }

  return example.paramC;
}

However, as it is, we will have an error in paramC of the last return with the following message:

Property 'paramC' does not exist on type 'ExampleType'. Did you mean 'paramA'?
  Property 'paramC' does not exist on type '{ paramA: "c" | "d"; paramB: number; }'.(2551)

In other words, even though the if() statement captures the second type (where paramA is "c" or "d"), at this point, example is still treated as a "composite type" between the two possibilities, and type narrowing is not performed correctly.

However, if I adjust the type definition to the following:

export type ExampleType =
  | {
      paramA: "a" | "b";
      paramB: number;
      paramC: number;
    }
  | { paramA: "c"; paramB: number }
  | { paramA: "d"; paramB: number };

Then the code works correctly and identifies the last example as the first available type only.

Therefore, it can be concluded that when paramA allows "c" or "d", TypeScript is unable to perform type narrowing correctly.

Activity

  1. MartinJohns commented on Oct 8, 2023

    @MartinJohns
    Contributor

    Duplicate of #31404.

  2. exoRift commented on Jan 30, 2025

    @exoRift

    I've devised a workaround to this issue

    #61075 (comment)

  3. locked as resolved and limited conversation to collaborators on Oct 22, 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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions