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

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

Closed
rentalhost opened this issue Oct 8, 2023 · 1 comment

Comments

@rentalhost
Copy link

πŸ”Ž 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.

@MartinJohns
Copy link
Contributor

Duplicate of #31404.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants