-
Notifications
You must be signed in to change notification settings - Fork 12.8k
Indirect narrowing of destructured discriminated union types works only for trivial cases. #48846
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
Comments
Just noticed that removing However there was a reason for having that optional Check this Playground, remove the question mark and watch the evil error unfold. The problem here may not be the one I initially thought of. Should I modify the issue or open a new one? Edit: also I have to admit that I filled up the issue while these discriminated unions were (are still) not fully clear to me. Now I can see that the problem may be the same as #48522, since in one case the discriminant |
To create a discriminated union, you must have a common literal property. But type Meal = { food: 'TANGERINE'; cutlery?: never } | { food: 'PIZZA'; cutlery?: never } | { food: 'PASTA'; cutlery: Cutlery }; |
See also: #43026 |
Oh I see. But anyway it's worth noting that if you always use direct guard clauses Since unions of literals as discriminant already work, under certain circumstances, wouldn't be reasonable to have them fully implemented? (Mainly to simplify the declaration of the discriminated union, which may get unnecessarily verbose if the discriminant is a union of many literals) |
See specifically: #43026 (comment) |
@MartinJohns thank you for the link. That issue is marked as I am closing the issue. |
The way to think about it is that, in this type: type Meal = { food: 'TANGERINE' | 'PIZZA'; cutlery?: never } | { food: 'PASTA'; cutlery: Cutlery } Logically, this type represents the disjunction function eat({ food, cutlery }: Meal) {
if (food === 'TANGERINE') return cutlery;
// what's for dinner? it's not tangerine, but might be pizza. therefore, still TangerineOrPizza | Pasta
if (food === 'PIZZA') return cutlery;
// again, what's for dinner? it's not pizza, but (as far as TS knows) might be tangerine;
// the fact that it isn't tangerine couldn't be preserved in terms of types alone (see above)
// therefore, we STILL have TangerineOrPizza | Pasta
return cutlery; // end result: this is not narrowed
} In order to deal with this problem, the type system would need to explode object types containing unions, e.g. |
@fatcerberus thanks for the insight! The problem of explosive types can be solved by setting a reasonable limit to the number of expanded types (like when your infinite recursion exceeds the maximum call stack size). Regarding the fact that the expanded types may not be an equivalent representation of the original union, how it comes? |
The main theoretical one is that "the existential is in the wrong place", that is, uncertainty about the nature of a container implies uncertainty about its contents, but the inverse is not true. By analogy: |
I think I get what you are saying. A possible solution to this problem may be making available a syntax that gives the expressive power to state whether a union is meant to be considered Something like |
Putting aside the nerdy theoretical details, I agree it’s a bit confusing that negative type guards don’t always compose in the obvious way. They only compose if each individual guard is able to eliminate some member of a union. Hence the seemingly contradictory situation of Good news is that it might be possible to make a utility type that does the necessary expansion to make this work. If I come up with something I’ll report back. |
@fatcerberus I'm rooting for you. |
Bug Report
Narrowing destructured discriminated union types using indirect control flow, works only for trivial cases in which the discriminant is a union of at most two types.
I found many issues related to discriminated unions, but they describe different problems.
🔎 Search Terms
discriminated
🕗 Version & Regression Information
Control Flow Analysis for Destructured Discriminated Unions has been introduced (or fixed) in TypeScript 4.6
The bug has been tested with the currently most recent version in the TypeScript Playground, which is v4.6.2
⏯ Playground Link
The discriminant is a union of at most two types: indirect narrowing works as expected.
The discriminant is a union of at least three types: indirect narrowing fails.
💻 Code
The discriminant is a union of at most two types and indirect narrowing works as expected:
The discriminant is a union of at least three types and indirect narrowing fails:
🙁 Actual behavior
Indirect narrowing doesn't work on destructured discriminated union when the discriminant is a union of at least three types.
🙂 Expected behavior
Destructured discriminated union should be narrowable indirectly, no matter how many types are involved in the discriminant.
If an implementation for a discriminant composed by an arbitrarily large number of types is not possible, I hope it is possible to at least raise the bar to five or even twenty types.
The text was updated successfully, but these errors were encountered: