Open
Description
Bug Report
Hello,
🔎 Search Terms
type narrowing, generic types, control flow analysis
🕗 Version & Regression Information
(see Playground) When trying to narrow generic types (unions), the control flow analysis is not aware of the narrowing. I also see that this PR: #43183 was supposed to address this.
Please keep and fill in the line that best applies:
- This is the behavior in every version I tried, and I reviewed the FAQ for entries about type narrowing
⏯ Playground Link
Playground link with relevant code
💻 Code
const INITIAL_STATE = {
1: 'test1',
2: 'test2',
};
type State = typeof INITIAL_STATE;
const stateReducer = <Type extends keyof State>(
state: State,
action: { type: Type; value: string } | { type: 'clear'; value: number },
) => {
if (action.type === 'clear') {
// action.value is "string | number", but should be "number"
return action.value;
}
return {
...state,
[action.type]: action.value,
};
};
stateReducer(INITIAL_STATE, { type: 'clear', value: 0 });
🙁 Actual behavior
action.value is "string | number"
🙂 Expected behavior
but should be "number"
Thanks in advance!
Metadata
Metadata
Assignees
Type
Projects
Milestone
Relationships
Development
No branches or pull requests
Activity
whzx5byb commentedon Sep 6, 2022
{ type: Type; value: string } | { type: 'clear'; value: number }
is not a discriminated union so it may not be narrowed as the way you expected. To make it work, you have to distribute over the generic typeType
.andrewbranch commentedon Sep 6, 2022
It’s a fair question why
type
isn’t a discriminant property, though. It’s not that every constituent in the union needs a unit type, because this is discriminable:The fact that
Type
is a type parameter, though constrained to a union of literal types, prevents us from recognizing the property as a discriminant. The conditional type distributes, yes, but it also kind of un-genericizes by filling in the constraint, which lets us recognize the property as a discriminant. I don’t immediately see a great reason why we couldn’t say that a type parameter whose constraint is a literal type is a valid discriminant... but these things have a way of having unexpected rippling effects.fatcerberus commentedon Sep 7, 2022
It's funny you say this because this was also my understanding for a long time. It's only due to past comments by @RyanCavanaugh that I found out the rule is the discriminant can either be a unit type or a union of unit types (thus why
keyof State
works as a discriminant). So I certainly wouldn't blame anyone who didn't realize that.It's interesting that that conditional type trick works. I would expect it to just defer since it's distributive over a type parameter.
wbt commentedon Nov 14, 2022
Here is another toy example demonstrating what I think is the same issue:
Search terms: Type guard narrowing assignable to parameter of type 2345
thetutlage commentedon Nov 22, 2022
Another one when using unions with generics.
I expect the
rounds: 1
parameter to be disallowed, since the property does not exists on theArgonConfig
.foxfirecodes commentedon Feb 13, 2023
any update on this? I find myself FREQUENTLY running into this issue, especially when dealing with things like events where there's multiple different types with different payloads. here's an example:
(ts playground)
I do this kinda thing all the time when making abstractions to handle multiple types of input/output, and it's a pain to have to typecast the return types
jcalz commentedon Aug 4, 2023
This looks like the same thing except the generic is constrained to something other than a union... but it should still probably be discriminable:
Playground link
reiss-d commentedon Sep 8, 2023
I think this is a slightly different example of the control flow not narrowing the generic, only it's not a property access:
Playground
Lordfirespeed commentedon Sep 10, 2024
And another toy example (I believe):
https://stackblitz.com/edit/typescript-repro-eqknuz?file=src%2Fmain.ts
gima commentedon Dec 1, 2024
"Type narrowing from control flow analysis doesn't backpropagate to another, type-dependent, conditional parameter's type".
The demonstration code below is identical* for both TypeScript and Flowtype.
🔗 TypeScript Playground link
Result: TypeScript's type checker doesn't manage to narrow the type.
🔗 Flowtype Playground link
Result: Flowtype's type checker manages to narrow the type correctly, as should be the case.
*) (the code is otherwise identical, except for changing "String" to "string" and "Number" to "number").
The code in question:
If needed, I can provide a real and useful, and not terribly complex code example of this, where a correctly narrowed type would avoid a superfluous null check (to narrow the possibility of a null value out of a variable).