TypeScript: Generics extending unions cannot be narrowed
TypeScript Version: 2.2.0-dev.20170126
Code
declare function takeA(val: 'A'): void;
export function bounceAndTakeIfA<AB extends 'A' | 'B'>(value: AB): AB {
if (value === 'A') {
takeA(value);
return value;
}
else {
return value;
}
}
Expected behavior: Compiles without error.
Actual behavior:
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 extends can 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?
About this issue
- Original URL
- State: closed
- Created 7 years ago
- Reactions: 188
- Comments: 56 (12 by maintainers)
Links to this issue
- typescript - Error 2349 when dealing with union type of generic T and function - Stack Overflow
- Typescript - How do I narrow type possibilities of a generic type in a switch statement? - Stack Overflow
- typescript - Type Inference on a function using a parameter - Stack Overflow
- typescript - Narrow related generic types - Stack Overflow
This issue is now fixed in #43183.
@RyanCavanaugh I feel like prioritizing speed over a sound type-system is kind of a contradiction here. We use TypeScript to have a type-system. If there are large gaps in it’s type-system, and I do consider this a large gap, TypeScript should correct them. Every omission like this hurts its usefulness as a tool we can rely on. It is its one, singular job to reason about types correctly.
That said: two years ago, it might’ve been true that this was not a common pattern. But two years later, these patterns are now everywhere. There’s even examples of these kinds of generics in TypeScript’s standard definitions and utility types.
The thing is, definitions don’t necessarily check against an implementation. You can just say something takes a type-parameter with a union type constraint, and this issue is essentially swept under the rug from a user-facing perspective. The external API-level side all type-checks fine.
But the pattern can’t be repeated in a real function implementation and have those types do their intended work. We should not need to slap
// @ts-ignore - TS being dumb againto force it to swallow its own valid types.It’s really time to fix this one. At least TS has incremental compilation now, so hopefully speed isn’t an issue now either.
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.
Here is an example that demonstrates the difference in behaviour between non-generic and generic functions:
@RyanCavanaugh please consider fixing this again
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.4 years later. I’m gonna cry I think 😃
@RyanCavanaugh, I have a more hypothetical question 🙂 Are you open to receiving suggestions for this issue? In principle, I would be interested in looking into this, but I have a few questions.
From your previous comment,
I gather that you think fixing this would be a large undertaking, but how big of an undertaking you think it would be? From everything, you can see, do you think it would be feasible to come up with a first draft within a month?
Are there any higher-level concepts that would have to be affected by a fix?
Of course, better type inference here would certainly incur some kind of performance cost, but are there any performance-pitfalls you can already see us running into with a fix?
Thank you in advance for spending time on those questions!
@RyanCavanaugh is there any plan to fix that? I encounter it a lot and it would be great if you could address that.
This would be great for specializing Props to React components. Contrived example:
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.Another simple example of a problem. With generic extending a union type, automatic type guards don’t work. See code in the playground.
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.
same issue, different example:
I realize this issue is closed, but I’m still getting this behavior. I’ll try and post a minimal working example but the full usage can be seen here.
TS playground link here
I have this type,
PrimitiveOrConstructor:and I’m using it as the generic constraint in this function, and I have to assign the variable with type
T extends PrimitiveOrConstructorin order to get narrowing:If I try to skip using the type variable it doesn’t work, even if I define an explicit type guard function:
@jhnns This is actually more closely related to another issue I’d raised, #21879. Basically, Typescript never considers the narrowing of a value, e.g.
valueas inplying anything about the type, e.g.T, that value has. The reason why is this:In that function call,
TisA & B, which means it will passisBand return'bar'. But the definition ofFoobarsays thatFoobar<A & B>should be'foo'instead.The “solution,” such as it is, is to use casting. Whether you do that by defining a particular type (e.g.
MapNullUndefinedor by just writing outas T extends null ? undefined : T, either works, but you basically are forced to tell the compiler that you have done this right, which means the compiler cannot correct you if you have not, in fact, done it right.Which drastically limits the value of conditional types, in my opinion, since they will almost-always, necessarily, be unsafe at least within the internals of the function. And plenty of cases where
A & Bisneverexist, which could be handled safely, but the TS team seems to be uninterested in going down that road.Can confirm generic union type guards aren’t working
Playground link
Another example. Code
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:
Hooray!
But ugh I think I’ve been referencing this issue for years now without realizing that this issue is specifically about narrowing values whose types are generic, and not about the type parameters themselves. Looks like I’ll have to go change a bunch of my SO answers to point to something more appropriate like #24085, #25879, #27808, and #33014.
Trying to work around the fact that generic types extending union types don’t get narrowed, I came up with the following code:
I would have expected this code to work:
Thank you in advance for any input you may have!
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.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?I think this is related:
Basically, it works if there’s a concrete type, but ignores the extends type bound when used as a generic.
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.Same issue here but user defined type: Code
Quoting the previous comment
In case doing the “right” thing proves to be too complex and will take (and most likely it will) a lot of time (or won’t be implemented at all) I’d be grateful (and probably many others) for considering the above suggestion.
I’ve spent most of my day today trying to figure out what mistake I have made in my types, then a couple of hours searching through the internet and in the end stumbled upon this thread to find out it wasn’t me 😅 (ok it was, because I tried to do sth that is not supported but you catch the drift 😅 )
@RyanCavanaugh hi, do you have any updates about this topic?
Maybe something has changed in the two years since you wrote!
It would be good if you could just give us a short information. Thank you very much.
I’m running into this issue a lot, and would love to see it fixed. In particular, I’d love for the type system to be able to track a
valueof typeTboth for its relationship withTand its possible (narrowed) type.My dream would be if both of these errors could go away:
A workaround worth noting is using a single overload with the generic type signature, before writing the function without the type parameter:
The flaw with this approach, of course, is that the body of the function is not checked to ensure the relationship specified by the type parameter
T. It’s also verbose. However, if you are doing casting at each site, you are also losing these guarantees, and being verbose.Edited to add: It really is a trade-off, because for some functions, a lot of type-checking is lost if you don’t have
Tin the implementation, and yet more casts may be needed.How should I write such code then?
Hello!
I think I fell on this same issue. Here’s my example:
Playground Link
Still related to this issue, I have an extended example of the above one where I also had a compiler error:
Playground Link
None of this is a production problem, though. I’m actually studying and playing around with typing and trying to provide the most intelligent inferences I can. I tried to add a feature to this package where some arguments could be a keyof something or a function mapper. This is where I fell in this limitation.
Just a minimal example that raises the error. I really think that it should be solve. On my case, it has a huge impact and force me to introduce a lot of assertions and type casting within my code, leading to a huge part of code not being checked correctly by the typing system…
Please, note that the following function would work:
A Playground would make things easier to work with.