Description
Bug Report
🔎 Search Terms
Union narrowing conditional false
🕗 Version & Regression Information
- This is the behavior in every version I tried, and I reviewed the FAQ for entries about common 'bugs' that aren't bugs
- Nightly version as of test time: v5.0.0-dev.20230109
⏯ Playground Link
Playground link with relevant code
💻 Code
(Note: naming conventions may be a bit inconsistent in this toy example with a smaller, more general-knowledge, domain)
//Types widely used throughout application; expansion would be incorrect
type G7Capital = {
CA: 'Ottawa'; FR: 'Paris'; DE: 'Berlin'; IT: 'Rome';
JP: 'Tokyo'; UK: 'London'; US: 'Washington DC'; EU: 'Brussels';
}
type G7Abbreviation = keyof G7Capital;
//Type used only in a few functions for handling a legacy edge case:
type G8Capital<A extends G7Abbreviation | 'RU'> = (
//Error ts(2536): Type 'A' cannot be used to index type 'G7Capital'.
//but on the side of the conditional where A is found, 'RU' *should be*
//narrowed out (it is not: that's the bug reported here)
//and that would make A === G7Abbreviation === keyof G7Capital
//which can be used to index 'G7Capital' so there should be no error.
A extends 'RU' ? 'Moscow' : G7Capital[A]
//A workaround is flipping the conditional order,
//but it's not intiutive to try this, expected to behave the same:
//A extends G7Abbreviation ? G7Capital[A] : 'Moscow'
);
🙁 Actual behavior
In its last uncommented instance, A is still of type G7Abbreviation | 'RU'
instead of just G7Abbreviation
producing error ts(2536): Type 'A' cannot be used to index type 'G7Capital'.
🙂 Expected behavior
On the side of the conditional where A is found, 'RU' should be narrowed out (it is not: that's the bug reported here) and that would make A === G7Abbreviation === keyof G7Capital which can be used to index 'G7Capital' so there should be no error.
Also, flipping the conditional sequence as seen in the workaround should not make a substantive difference in any conditional expression where either side of the conditional can be described with relative ease.
Deduplication discussion
Not a duplicate of #44401 as it is not fixed by #44771, nor of #44382 as it did not change between versions 4.2.3 and 4.3.2. This is also not a duplicate of issues around control flow analysis, especially with the optional chaining or ternary operators in control-flow analysis of JavaScript.
Activity
RyanCavanaugh commentedon Jan 9, 2023
We'd need negated types to be able to handle this properly, since the correct type to give
A
in the false branch is still a type parameter. Changing it to a concreteG7Abbreviation
would break (many) other things.Without loss of generality you can write
G7Capital[A & G7Abbreviation]
wbt commentedon Jan 10, 2023
When an incoming type param is of the form
A extends B | C
(for notation convenience below,type D = B | C
) and you have an extension of formA extends B ? T : F
I would expect that occurrences ofA
in expressionT
are treated asB
and occurrences ofA
in expressionF
are treated asExclude<D, B>.
I don't think negated types are needed for that.To be more explicit, in this particular case I would also then expect that
Exclude<D, B>
would be equivalent toExclude<B | C, B>
which would further be equivalent toC
.RyanCavanaugh commentedon Jan 10, 2023
Understood that that was your expectation. That is not what happens.
wbt commentedon Jan 10, 2023
I understand that is not what happens; the difference between what I think is a reasonable expectation other developers would likely share (if it's not, some description of why it's not a reasonable expectation would be appreciated!) and the reality of what happens is reported here as a bug.
fatcerberus commentedon Jan 11, 2023
I'm not convinced this needs negated types, at least not for the simple union case; it feels like it might be enough to simply re-constrain the type parameter in the false branch of the conditional, e.g. from
A extends G7Abbreviation | 'RU'
toA extends G7Abbreviation
. As this already works:so if the compiler could automatically apply that
Exclude
to the constraint ofA
it would solve this, I think.wbt commentedon Jan 11, 2023
I should also clarify that while in this demonstration example, the generic type parameter constraint (
D
) is a union of a union and a constant, some of the motivating examples in actual code have two unions.Keeping the same example domain, here is a slightly less minimal example which demonstrates this:
Fixing the issue reported above in the way @fatcerberus describes would also fix this case.
I don't think this requires having negated types and I don't think this Issue should be closed as a duplicate of nothing.
wbt commentedon Jan 11, 2023
As another note, introducing negated types wouldn't automatically fix this issue either - someone would still have to go in and change the definition for how the type parameter is constrained in the false branch, just as they would to add
Exclude
.RyanCavanaugh commentedon Jan 12, 2023
Sure. "re-constraining" isn't really a thing, though. That'd be a separate can of worms; it might be good to collate some use cases for that behavior if it'd be useful elsewhere.
wbt commentedon Jan 13, 2023
Maybe there's another term instead of "re-constraining" that is applied to further constrain A in the true side of the conditional (to the intersection of its original limits and the type it
extends
for that conditional), and a similar process could be applied on the false side.typescript-bot commentedon Jan 16, 2023
This issue has been marked as a 'Duplicate' and has seen no recent activity. It has been automatically closed for house-keeping purposes.
wbt commentedon Jan 16, 2023
I don't think this should be CLOSED WONTFIX (as a duplicate of nothing).