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
Generics extending unions cannot be narrowed #13995
Argument of type 'AB' is not assignable to parameter of type '"A"'.
It works correctly if I just use value: 'A' | 'B' as the argument to bounceAndTakeIfA, but since I want the return value to type-match the input, I need to use the generic (overloading could do it, but overloading is brittle and error-prone, since there is no error-checking that the overload signatures are actually correct; my team has banned them for that reason). But (I'm guessing) since AB extends 'A' | 'B', narrowing doesn't happen. In reality, I am just using extends to mean AB ⊂ 'A' | 'B', but extendscan mean more than that, which (I suspect) is why TS is refusing to narrow on it.
Some alternative to extends that more specifically means ⊂ (subsets maybe?) would probably be the best solution here?
Got the same (I think) problem with a more elaborated example:
interfaceWithNumber{foo: number;}interfaceWithString{bar: string;}typeMyType=WithNumber|WithString;interfaceParameter<CextendsMyType=MyType>{container: C}functionisParameterWithNumberContainer(arg: Parameter): arg is Parameter<WithNumber>{returntypeof(arg.containerasWithNumber).foo==="number";}functiontest(arg: Parameter<WithNumber|WithString>){if(isParameterWithNumberContainer(arg)){arg.container.foo;return;}/* * Error: * Property 'bar' does not exist on type 'MyType'. * Property 'bar' does not exist on type 'WithNumber'. */arg.container.bar;}
In my opinion, it is impossible that arg will be something else than a Parameter<WithString>. Typescript however still thinks that it is a Parameter<WithNumber | WithString> and thus throws an error when trying to access arg.container.bar.
Reacted by KittenWithHerbs, Kaitlin Schaer, Stacey Gammon, vike2000, C. T. Lin, Alisa Sireneva, Bitcollage, Luiz Felipe Frazão, Rémi Pace, Mohammad Kermani and 2 more
TL;DR from design discussion: It's somewhat obvious what the "right" thing to do is, but would require a large rework of how we treat type parameters, with concordant negative perf impacts, without a corresponding large positive impact on the actual user-facing behavior side.
If new patterns emerge that make this more frequently problematic, we can take another look.
Reacted by Emmanuel Meric de Bellefon
Reacted by ethernet, Slava, Kincaid O'Neil, Tim Whitbeck, Ádám Barancsuk, Micah Zoltu, Krunal Sheth, Jakub Wolny, James Bromwell, Cyril Silverman and 59 more
Reacted by John, Kevin Ryan, Jay Phelps, sam-perez, Brian Kim, Tim Brown and Simon Lagos
By using return num * 2 as T; and return () => num() * 2 as T;, unfortunately. There is no better way, because while TS will narrow num it won’t narrow T for you.
Reacted by Denis Zhbankov, Nycki, Yehyoung Kang and JomoPipi
Reacted by Ran Lottem, KittenWithHerbs, Tyler Rick, Olivier Tassinari, Linus Unnebäck, tyankatsu, Stan Mots, C. T. Lin, François Billioud, Bitcollage and 11 more
This would be great for specializing Props to React components. Contrived example:
type Type = 'a' | 'b';
type AShape = { a: 'a' };
type BShape = { b: 'b' };
type Props<T extends Type> = {
type: T,
shape: T extends 'a' ? AShape : BShape,
};
class Test<T extends ID> extends React.Component<Props<T>> {
render() {
const { type, shape } = this.props;
switch (type) {
case 'a':
return <>{shape.a}</>; // Ideally would narrow `shape` here, instead of `AShape | BShape`
default:
return <>{shape.b}</>;
}
}
}
<T type="a" shape={{ a: 'a' }} /> // No error in ideal case
<T type="a" shape={{ b: 'b' }} /> // error in ideal case
Reacted by Tim Whitbeck, Ádám Barancsuk, Mehdi Lahlou, Stacey Gammon, Brady O'Connell, Johnson, Isaac Donoghue, Will Dembinski, Philipp Mudra, quixot1c and 2 more
Reacted by Miloš Lajtman, Ran Lottem, Jakub Wolny, Phil Eaton, Benjamin Goering, Adam Wagner, Peter Caisse, Tyler Rick, Ralf Vogler, Denis Frenademetz and 19 more
I think this is the same issue, this came from a real use case but I've reduced it to a toy example to better demonstrate:
interfaceFoo{stringVal: string;stringArrayVal: string[];numberArrayVal: number[];}typeKeysWithType<T,V>={[KinkeyofT]: T[K]extendsV ? K : never}[keyofT];typeArrayPropertyOf<T>=KeysWithType<T,Array<any>>;typeArrayTypeOfPropertyOf<T,KextendskeyofT>=T[K]extendsArray<infer U> ? U : never;/// type checksconstobj: Foo={stringVal: 'hello',stringArrayVal: ['world'],numberArrayVal: [1,2,3]}leta: ArrayTypeOfPropertyOf<Foo,'numberArrayVal'>;// as expected, type narrowed to number.functionprintArrayKeyWorking(key: ArrayPropertyOf<Foo>){switch(key){case'stringArrayVal':
case'numberArrayVal':
case'stringVal': // as expected: Type '"stringVal"' is not comparable to type '"numberArrayVal" | "stringArrayVal"'.console.log(key);}}functionprintArrayKeyNotWorking<KextendsArrayPropertyOf<Foo>>(key: K){switch(key){case'stringArrayVal':
case'numberArrayVal':
case'stringVal': // no errorconsole.log(key);}}functiontransformChildArrayValue<KextendsArrayPropertyOf<Foo>>(key: K,value: ArrayTypeOfPropertyOf<Foo,K>){switch(key){case'stringArrayVal':
returnvalue.repeat(2);// not expected: Type '"stringVal"' is not comparable to type '"numberArrayVal" | "stringArrayVal"'.case'numberArrayVal':
returnvalue**2;// not expected: The left-hand side of an arithmetic operation must be of type 'any', 'number', 'bigint' or an enum type.case'stringVal':
returnkey;}}
Is there actually a workaround existing for this? The real case I'm having is the same kind of problem as the last function where I am unable to correctly type the key name and value as the two parameters as the type cannot be correctly narrowed. Note this is part of a library that I want to enforce good types for the downstream developer so I'm looking for generic solution not one specific to this particular problem
// Type definitionsenumCarType{sedan='sedan',suv='suv',truck='truck'}typeCarSedan={type: CarType.sedan;randomKey1?: string;}typeCarSUV={type: CarType.suv;randomKey2?: number;}typeCarTruck={type: CarType.truck;randomKey3?: string;}//. Type guard using the key "type" to differentiate the union typesconstisCarTruck=<TruckextendsCarTruck,OthersextendsCarSedan|CarSUV,AllextendsTruck|Others>(car: All): car is Exclude<All,Others>=>(carasCarTruck)?.type===CarType.truck;// SubsettypeUnion=CarTruck|CarSedan|CarSUV// Example 1: In a normal usage its workingconstfunc1=(car: Union)=>{isCarTruck(car)
?
car// CarTruck
:
car// CarSedan | CarSUV}// Example 2: With generic extends it doesnt workconstfunc=<TextendsUnion>(car: T)=>{isCarTruck(car)
?
car// "Exclude<T, CarSedan | CarSUV>"" ----> CarTruck
:
car;// "T extends Union" ----> ?????}
typeTypes="1"|"2"|"3"typeXYZ<TextendsTypes>={type: T;x: Textends"1"
? number
: Textends"2"
? string
: Textends"3"
? boolean
: never;};functiony<TextendsTypes>(x: XYZ<T>){if(x.type==='1'){x.x+1// fails because it's number or string or boolean}}
Reacted by Robert Gruner, Douglas Gaskell, DirtyWhite, Zach Stence and Eliot Austin-Forbes
Narrowing one variable of a type parameter's type does not give you sound information about another variable of the same type parameter's type.
Reacted by Thiago Oliveira Santos, Douglas Gaskell, AverageHelper, Jackson, Tyler Sheaffer, Guillaume Humbert, Adam Borek, Eliot Austin-Forbes, Max Duval, Alex Guerra and 1 more
constActions={Action1: 'Action1',Action2: 'Action2',}asconst;typeAction=ValueOf<typeofActions>;// Some dynamic parameterstypeParamConstraints={[Actions.Action1]: {name: string,age: number},[Actions.Action2]: {company: string}};functiondoSomething<TActionextendsAction>(action: TAction,param: ParamConstraints[TAction]){if(action==='Action1'){param.name// Property 'name' does not exist on type '{ name: string; age: number; } | { company: string; }'.}if(action==='Action2'){param.company// same error}switch(action){case'Action1':
param.name// same errorbreak;case'Action2':
param.company// same errorbreak;}}// Expected error about the wrong structure for the 2nd argumentdoSomething('Action1',{company: 'test'})
This seems to enforce the contract for callers (ie. I must call doSomething() with the correct arguments as dictated by ParamConstraints) . Shouldn't this be safe for narrowing inside of doSomething()?
To me that looks like self-sabotage by intentionally casting to something not intended? I'm not sure if I would consider that a case that needs defending against for that example, but I'm very likely wrong 😅
I guess this spawns 2 questions for you:
Is this sort of narrowing even possible/supported in TS?
If it is. Is there further constraint that I need to make this safe? Or how can this be done? (Does it have a name?)
Reacted by Tim Brown, Alek Angelov, AverageHelper and Jackson
This is the work-around I'm using. The key function in the following is the asUnion function, and its example use in discriminateExample. Explanation below.
typeTypes={'Num': number'Str': string}typeNames=keyofTypes// 'Num' | 'Str'typeInner<TextendsNames>=Types[T]interfaceBox<TextendsNames>{type: Tvalue: Inner<T>}// Example of generic operation on BoxfunctionunboxExample<TextendsNames>(box: Box<T>): Inner<T>{// `box.value` is correctly inferred with type `Inner<T>`returnbox.value}functionasUnion<TextendsNames>(box: Box<T>): Textendsany ? Box<T> : never{returnboxasany}functiondiscriminateExample<TextendsNames>(box: Box<T>){const{ type, value }=asUnion(box);switch(type){case'Num': returnvalue+1;// `value` narrowed to `number`case'Str': returnvalue+' text';// `value` narrowed to `string`}}
For my use case, I have a generic Box<T> container, where T is a type name (Num or Str in the example, to keep things simple).
I have some operations on boxes that I want to implement generically -- unboxExample is an example. And I have some operations on boxes where I want to discriminate on the different types of boxes and implement each case individually -- discriminateExample is an example. The issue, as raised in this thread, is that it's hard to do both kinds of operations on the same types: if Box<T> is a union then generic operations require casting, and if Box<T> is not a union then you can't discriminate on the individual possibilities.
The solution here is the asUnion function, which internally does a cast from the generic form to the union form, but at least the cast is only in one place. The return type of asUnion uses a conditional type to expand out the discriminated union based on all the possible type names T.
For example, if the argument of asUnion is Box<'Num' | 'Str'> then the return type is Box<'Num'> | Box<'Str'>, which is then compatible with TypeScript's narrowing logic.
The fix seems very incomplete to me. A simple re-assignment of the narrowed variable will break it:
type Type = 'a' | 'b';
let testVariable: 'a'
function testTypeNarrowing<T extends 'a' | 'b'>(t: T): 'a' | null {
if (t === 'a') {
// this assignment works, making you believe t has been correctly narrowed
testVariable = t
// t has not actually been narrowed here
const x = t
return x //compilation error, but returning t itself woudl work!
}
return null
}
This of course causes all sorts of related problems. In light of this, shouldn't this issue be reopened for having a main source of reference on these kind of issues?
Reacted by brkaisin, julidemab12, martintamb12, hugodendievelb12, Andjic Djordje, Nicolas Lurkin, mathias-michielsen-b12, Olivier Martin, katarinamilacic, cloudgazing and 1 more
This also doesn't work with narrowing conditional types (tested with both enums and unions in TS 5.5):
function getConcreteEnumImpl<TEnum extends MyEnum>(myEnum: TEnum): SpecializedTypeOfEnum<TEnum> {
switch (myEnum) {
case MyEnum.bar: {
return new BarEnumSpecialization(); <-- Typing error because TS does not narrow `TEnum` to `bar`
}
case MyEnum.foo: {
return new FooEnumSpecialization();
}
default: throw new Error();
}
}
Marcos Pereira (@marcospgp) The problem is that you’re expecting K to be one and only exactly one, known, keyof Action. That isn’t necessarily true. Consider persist<"ARM" | "x86"> (either written that way, or because someone has a variable typed as "ARM" | "x86"): Because K is "ARM" | "x86", data can be Action["ARM" | "x86"], which is the same as Action["ARM"] | Action["x86"].
You need to come up with a structure that actually enforces that each action goes only with its own data. You can’t do that with separate function arguments, but you can do it with a single object. To do it, though, you need a union of the specific type-pairings you’re looking at:
You can automate the creation of Actionable by using distributive types, though you’ll need a helper type for it because this is one of those weird times where a type alias isn’t just an alias in TS: you need the somewhat arcane distributive property. A type is distributive, i.e. it handles a union argument by applying the type to each member of the union separately and then unioning those results, if its argument isn't constrained.
// distributive type because <K> isn't constrainedtype_ActionableOf<K>=[action: K,data: Action[K]];// distributes, so each keyof Action is handled separately,// so equivalent to _ActionableOf<"ARM"> | _ActionableOf<"x86"> | …// which equals ["ARM", Action["ARM"]] | ["x86", Action["x86"]] | …typeActionable=_ActionableOf<keyofAction>;
With this, you can write a persist function that is actually typesafe.
See this in a Playground. Note that despite destructuring with ..., this function sees the two arguments as a single Actionable object (which is defined as a 2-ple). TS can narrow that 2-ple based on its members (I was honestly surprised to discover this, I thought you’d have to work with the 2-ple as a single object, i.e. actionable[0] instead of action and actionable[1] instead of data, in order to get narrowing).
Also, in this case, currently, there isn’t actually a need for the generic on persist, either. Writing function persist(...[action, data]: Actionable) would work just as well. The generic would be useful if, say, you wanted to return something different depending on A.
Activity
masaeedu commentedon Mar 22, 2017
But
extendsalready does mean subset. If I'm not mistaken,extendsis simply TypeScript's implementation of bounded quantification.Regarding:
Do you have a specific example of behavior where you'd do
T extends Ufor some T not assignable to U?Nimelrian commentedon Jan 18, 2018
Got the same (I think) problem with a more elaborated example:
In my opinion, it is impossible that arg will be something else than a
Parameter<WithString>. Typescript however still thinks that it is aParameter<WithNumber | WithString>and thus throws an error when trying to accessarg.container.bar.RyanCavanaugh commentedon Feb 6, 2018
TL;DR from design discussion: It's somewhat obvious what the "right" thing to do is, but would require a large rework of how we treat type parameters, with concordant negative perf impacts, without a corresponding large positive impact on the actual user-facing behavior side.
If new patterns emerge that make this more frequently problematic, we can take another look.
lingz commentedon Jun 18, 2018
Again appeared in #25039
mifopen commentedon Nov 16, 2018
How should I write such code then?
krryan commentedon Nov 16, 2018
By using
return num * 2 as T;andreturn () => num() * 2 as T;, unfortunately. There is no better way, because while TS will narrownumit won’t narrowTfor you.mysticatea commentedon Jan 9, 2019
tscversion: 3.2.2I have encountered this problem. It's much odd because it says "'a' does not exist" even if it's inside of
if ("a" in t)block.DerGernTod commentedon Feb 6, 2019
same issue, different example:
rsolomon commentedon Feb 14, 2019
This would be great for specializing Props to React components. Contrived example:
OliverJAsh commentedon Mar 29, 2019
Here is an example that demonstrates the difference in behaviour between non-generic and generic functions:
zakhenry commentedon Jun 22, 2019
I think this is the same issue, this came from a real use case but I've reduced it to a toy example to better demonstrate:
Is there actually a workaround existing for this? The real case I'm having is the same kind of problem as the last function where I am unable to correctly type the key name and value as the two parameters as the type cannot be correctly narrowed. Note this is part of a library that I want to enforce good types for the downstream developer so I'm looking for generic solution not one specific to this particular problem
Any tips for a workaround greatly appreciated!
42 remaining items
Cauen commentedon Mar 13, 2022
Another example. Code
leonchabbey commentedon May 20, 2022
Same issue here but user defined type: Code
alekangelov commentedon Jul 7, 2022
Can confirm generic union type guards aren't working
Playground link
douglasg14b commentedon Jul 11, 2022
From the look of this thread, the issue is NOT really resolved at this point, and we cannot narrow dependent types?
RyanCavanaugh commentedon Jul 11, 2022
A very high proportion of comments here are misunderstanding a fundamental aspect of how union-constrained generics work.
It is not safe to write code like this
because a caller can legally write
Narrowing one variable of a type parameter's type does not give you sound information about another variable of the same type parameter's type.
douglasg14b commentedon Jul 12, 2022
Ryan Cavanaugh (@RyanCavanaugh) Gotcha, thanks for that explanation! I can see how that's unsafe.
What about this example?
This seems to enforce the contract for callers (ie. I must call
doSomething()with the correct arguments as dictated byParamConstraints) . Shouldn't this be safe for narrowing inside ofdoSomething()?Or is this something entirely different?
RyanCavanaugh commentedon Jul 12, 2022
No
douglasg14b commentedon Jul 12, 2022
Thank you for the response!
To me that looks like self-sabotage by intentionally casting to something not intended? I'm not sure if I would consider that a case that needs defending against for that example, but I'm very likely wrong 😅
I guess this spawns 2 questions for you:
coder-mike commentedon Dec 19, 2022
This is the work-around I'm using. The key function in the following is the
asUnionfunction, and its example use indiscriminateExample. Explanation below.For my use case, I have a generic
Box<T>container, whereTis a type name (NumorStrin the example, to keep things simple).I have some operations on boxes that I want to implement generically --
unboxExampleis an example. And I have some operations on boxes where I want to discriminate on the different types of boxes and implement each case individually --discriminateExampleis an example. The issue, as raised in this thread, is that it's hard to do both kinds of operations on the same types: ifBox<T>is a union then generic operations require casting, and ifBox<T>is not a union then you can't discriminate on the individual possibilities.The solution here is the
asUnionfunction, which internally does a cast from the generic form to the union form, but at least the cast is only in one place. The return type ofasUnionuses a conditional type to expand out the discriminated union based on all the possible type namesT.For example, if the argument of
asUnionisBox<'Num' | 'Str'>then the return type isBox<'Num'> | Box<'Str'>, which is then compatible with TypeScript's narrowing logic.thierry-stul commentedon Jun 5, 2024
The fix seems very incomplete to me. A simple re-assignment of the narrowed variable will break it:
This of course causes all sorts of related problems. In light of this, shouldn't this issue be reopened for having a main source of reference on these kind of issues?
Woodz commentedon Jun 25, 2024
This also doesn't work with narrowing conditional types (tested with both enums and unions in TS 5.5):
Full TS playground link
marcospgp commentedon Jun 25, 2025
was also surprised to find out typescript won't infer a more specific type for a generic index access:
inside the if clause above, data is still Action[K] instead of Action["ARM"]
krryan commentedon Jun 25, 2025
Marcos Pereira (@marcospgp) The problem is that you’re expecting
Kto be one and only exactly one, known,keyof Action. That isn’t necessarily true. Considerpersist<"ARM" | "x86">(either written that way, or because someone has a variable typed as"ARM" | "x86"): BecauseKis"ARM" | "x86",datacan beAction["ARM" | "x86"], which is the same asAction["ARM"] | Action["x86"].For example,
See this Playground example.
You need to come up with a structure that actually enforces that each action goes only with its own data. You can’t do that with separate function arguments, but you can do it with a single object. To do it, though, you need a union of the specific type-pairings you’re looking at:
You can automate the creation of
Actionableby using distributive types, though you’ll need a helper type for it because this is one of those weird times where a type alias isn’t just an alias in TS: you need the somewhat arcane distributive property. A type is distributive, i.e. it handles a union argument by applying the type to each member of the union separately and then unioning those results, if its argument isn't constrained.With this, you can write a
persistfunction that is actually typesafe.See this in a Playground. Note that despite destructuring with
..., this function sees the two arguments as a singleActionableobject (which is defined as a 2-ple). TS can narrow that 2-ple based on its members (I was honestly surprised to discover this, I thought you’d have to work with the 2-ple as a single object, i.e.actionable[0]instead ofactionandactionable[1]instead ofdata, in order to get narrowing).Also, in this case, currently, there isn’t actually a need for the generic on
persist, either. Writingfunction persist(...[action, data]: Actionable)would work just as well. The generic would be useful if, say, you wanted to return something different depending onA.marcospgp commentedon Jun 26, 2025
Kevin Ryan (@krryan) it seems the underlying issue is there is no way to constrain a type to be a single key
jcalz commentedon Jun 26, 2025
which is #27808