You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Type narrowing of generic types in control flow analysis #50652
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
{ 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 type Type.
It’s a fair question whytype 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.
It’s not that every constituent in the union needs a unit type
It's funny you say this because this was also my understanding for a long time. It's only due to past comments by Ryan Cavanaugh (@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.
Here is another toy example demonstrating what I think is the same issue:
interfaceUpdateReport<Bextendsboolean=true>{newValue: string;oldValue: Bextendstrue ? string : undefined;}exportconstprocessReport=function<Bextendsboolean>(report: UpdateReport<B>,hasOldValue: readonly[B],){if(hasOldValue[0]){//Error TS(2345): Argument of type 'UpdateReport<B>'//is not assignable to parameter of type 'UpdateReport<true>'.//Type 'B' is not assignable to type 'true'.//Type 'boolean' is not assignable to type 'true'.//However, within this conditional check / type guard,//TS should be able to figure out that B is 'true'.letoldValue=getOldValueFromReport(report);//...}}constgetOldValueFromReport=function(report: UpdateReport<true>){returnreport.oldValue;};
Search terms: Type guard narrowing assignable to parameter of type 2345
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:
typeEventType='NEW_MESSAGE'|'NEW_USER'typeAppEvent=|{type: 'NEW_MESSAGE';message: string}|{type: 'NEW_USER';user: string}functionfetchEvents<TypeextendsEventType>(eventType: Type): Extract<AppEvent,{type: Type}>{if(eventType==='NEW_MESSAGE'){// this errors, because typescript can't figure out that Type must be 'NEW_MESSAGE'return{type: 'NEW_MESSAGE',message: 'hello, world',}}elseif(eventType==='NEW_USER'){// this errors, because typescript can't figure out that Type must be 'NEW_USER'return{type: 'NEW_USER',user: 'rayzr',}}else{thrownewError(`Unknown event type: ${eventType}`)}}
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
This looks like the same thing except the generic is constrained to something other than a union... but it should still probably be discriminable:
typeFoo<Textends{}>={a: undefined,b: string}|{a: T,b: number}functionfoo<Textends{}>(f: Foo<T>){if(f.a==null){f.b// should be string, is actually string | number}}
I think this is a slightly different example of the control flow not narrowing the generic, only it's not a property access:
declarefunctiontakesString(value: string): voidconstdata={a: 'foo',b: 1}typeData=typeofdatafunctiongetValue_generic<KextendskeyofData>(key: K){if(key==='a'){key// K extends "a" | "b"constvalue=data[key]// { a: string; b: number }[K]takesString(value)// Argument of type 'string | number' is not assignable to parameter of type 'string'.}}functiongetValue_non_generic(key: keyofData){if(key==='a'){key// "a"constvalue=data[key]// stringtakesString(value)// ok}}
🔗 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:
typeOneOfTwo=String|Number;typeConditionalTybe<T,TRUETYBE,FALSETYBE>=TextendsString ? TRUETYBE : FALSETYBE;functionfunktion<TextendsOneOfTwo>(arg1: T,arg2: ConditionalTybe<T,true,false>){if(typeofarg1=="string"){if(arg2===true){}if(arg2===false){}// why is there no error here?}}funktion('str',true);funktion('str',false);// correctly shows an error here.funktion(1,true);// correctly shows an error here.funktion(1,false);
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).
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
typeisn’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
Typeis 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 Ryan Cavanaugh (@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 Stateworks 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: 1parameter 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).