Repository navigation
Generic enumerated type parameter narrowing (conditional types) #24085
Description
Search Terms
conditional type inference enum enumerated narrowing branching generic parameter type guard
Suggestion
Improve inference / narrowing for a generic type parameter and a related conditional type.
I saw another closed-wontfix issue requesting generic parameter type guards, but a type guard should not be necessary for this case, since the possible values for the generic are enumerated.
Use Cases
(Names have been changed and simplified)
I have a method that takes a KeyType (enumerated) and a KeyValue with type conditionally based on the enumerated KeyType.
Depending on the KeyType value, the code calls method(s) specific to that type.
The TS compiler is unable to tell that after I have checked the enumerated KeyType, the type of the KeyValue (string, number, etc) is known and should be able to be passed to a function that only accepts that specific KeyValue type.
Examples
const enum TypeEnum {
String = "string",
Number = "number",
Tuple = "tuple"
}
// The issue also occurs with
// type TypeEnum = "string" | "number" | "tuple"
interface KeyTuple { key1: string; key2: number; }
type KeyForTypeEnum<T extends TypeEnum>
= T extends TypeEnum.String ? string
: T extends TypeEnum.Number ? number
: T extends TypeEnum.Tuple ? KeyTuple
: never;
class DoSomethingWithKeys {
doSomethingSwitch<TType extends TypeEnum>(type: TType, key: KeyForTypeEnum<TType>) {
switch (type) {
case TypeEnum.String: {
this.doSomethingWithString(key);
break;
}
case TypeEnum.Number: {
this.doSomethingWithNumber(key);
break;
}
case TypeEnum.Tuple: {
this.doSomethingWithTuple(key);
break;
}
}
}
doSomethingIf<TType extends TypeEnum>(type: TType, key: KeyForTypeEnum<TType>) {
if (type === TypeEnum.String) {
this.doSomethingWithString(key);
}
else if (type === TypeEnum.Number) {
this.doSomethingWithNumber(key);
}
else if (type === TypeEnum.Tuple) {
this.doSomethingWithTuple(key);
}
}
private doSomethingWithString(key: string) {
}
private doSomethingWithNumber(key: number) {
}
private doSomethingWithTuple(key: KeyTuple) {
}
}
This should compile without errors if TS was able to tell that the switch statements or equality checks limited the possible type of the other property.
I lose a lot of the benefits of TS if I have to cast the value to something else. especially if I have to cast as any as KeyForTypeEnum<TType> as has happened in my current codebase.
If I'm doing something wrong or if there's already a way to handle this, please let me know.
Checklist
My suggestion meets these guidelines:
[X] This wouldn't be a breaking change in existing TypeScript / JavaScript code
[X] This wouldn't change the runtime behavior of existing JavaScript code
[X] This could be implemented without emitting different JS based on the types of the expressions
[X] This isn't a runtime feature (e.g. new expression-level syntax)
Activity
krryan commentedon May 14, 2018
Seems related to #21879, and possibly #20375, which are pretty high priorities in my mind, too. Absolutely agreed that we really want something like this to be possible. A common use-case in our code is mapping functions, that take a union and map each possible value in the union to the corresponding value in another union. As an example, a function that maps
'1' | '2' | '3'to1 | 2 | 3. You can write a conditional type for this withBut this runs into a couple of problems: TS won't narrow
Char, TS won't recognize1asCorrespondingNumeralOf<Char>incase '1'.There really ought to be a type-safe way to write these kinds of functions, seeing as there is a type-safe way to describe them.
(And in case anyone thinks function overloads are a solution here, keep in mind that those aren't really any more type-safe than just using casting here, and in any event, those lack the ability to handle arbitrary subsets of the first union and map them to the corresponding subset of the second union. Not too bad when looking at three cases, but I recently wrote something very much like this that handled 28 cases.)
JakeTunaley commentedon Jul 23, 2018
This would be really useful for typing
addEventListenerpatterns. Here's a sample of real-world code where I ran into this issue:mattmccutchen commentedon Oct 11, 2018
After reading #27808, I realized that the original example is unsound. A caller can just do:
We'd need to express somehow that
TTypemust be (in this scenario) a singleton type. Compare to #25879.jack-williams commentedon Oct 11, 2018
I remember thinking about this when a thread regarding multiple use type parameters came up. Perhaps something like: uniform types. Types inhabited only by values that behave uniformly under operations such as
typeof.We write
T*, where*is a predicate on types that holds when:x,yinT.typeof x === typeof y.So
string*is valid, so isnumber*, but notstring | number.Example:
It might even be possible to specify the operation, so we could include equality for enums.
We write
T=, such thatx,yinT.x === y.jack-williams commentedon Mar 10, 2019
I have a very experimental PR that is capable of type-checking the original example: #30284
tadhgmister commentedon Apr 7, 2019
Can get around this by using an interface to map strings to corresponding types instead of a ternary chain. Not sure why generic type that is constrained by a union doesn't narrow though, but using a temporary variable can get around that as well:
For the original case it's a little more complicated, to narrow one variable
typeneeds a way to relate to the type of the other, which means we have to have a variable that actually implements this pseudo interface:tadhgmister commentedon Apr 7, 2019
Generalized function
pairedto turn this form of mapping into a data structure that works well in a switch statement and works in strict null checks, along with working example of JakeTunaley use case.I don't think with typescript it would be possible to fully match the relationship between 2 variables but it would be nice if instead of having a function like
pairedyou could just dolet pair = {evt, listener}and based on the generics involved it would correctly deduce the possible values of that structure, that would be great.krisdages commentedon Jun 4, 2019
The "extends oneof" syntax mentioned in one of the linked issues seems pretty interesting.
If
<T extends oneof Union = Union>could work in general that could also eliminate an awkward pattern I'm having to use with constraints in interfaces:BTW, this doesn't fix the narrowing issue inside the body, but for validating the correspondence of the parameters, this seems to work, now that rest parameters can be inferred:
Ranguna commentedon May 16, 2020
Tadhg McDonald-Jensen (@tadhgmister) I'm sorry to say but your code doesn't compile.
Ranguna commentedon May 16, 2020
Also adding my two cents:
https://www.typescriptlang.org/play?ts=3.9.2#code/KYOwrgtgBAou0G8BQVVQIIBoBCmDCSAvkkgMYD2IAzgC5QCyAhgA7PABOUAvFMmlAG04kAHToAugC4oACkbsA5tNrsAliAUBKbgD4oAOUgAjDnMWbMKNEPgjsU2fKVR4J9tq56AhE8v8bongOZs5G5OQANsCMIB56AESqNADkVFDxUADUjooA-Ok07GDAGdLxAGaMEVQlFkQkFNR0pFURTKwc3FAAPAAqUMAAHjSgACZpANbAAJ7k5VA002xzDCxs7DoyU9PSvZhQTtIACvKMEMAj7FTdi8vz7esCveI6AgAM4prSAEoXYOwgXpLYA3YErB4cJ4vXRWVAQ9gCbbiEKaADcSCAA
Basically I'm trying to use a mapper of enum -> function, where each function receives the same number of arguments and their types and return values depend on the enum.
I can't call the mapper from a function that accepts the key of the mapper and the value of the respective function. The compiler complains that the result of calling the mapper is not assignable to the expected return type (dependent on the enum passed) because TS creates a union of the return types instead of actually inferring them.
Workaround is to cast the the result of calling the mapper to
any. Unfortunately you'll lose type safety this way, so you'll have to make sure you know what you're doing, which we can all admit that we never really know 👍tadhgmister commentedon May 17, 2020
Luis Pais (@Ranguna) pretty sure it does: Playground Link. How were you trying to compile it?
Ranguna commentedon May 17, 2020
Tadhg McDonald-Jensen (@tadhgmister) Sorry, I should've been more specific. I was talking about this code:
(ignoring the missing
impossiblefunction)tadhgmister commentedon May 17, 2020
Oh I see, it looks like it worked the way I wanted it in version 3.3 but not in 3.5 or newer, not sure how I managed that since I thought I started using typescript when it was at 3.5.
Still doesn't change that your function example is something I have used in the past and my solution there doesn't extend to cases like that unfortunately.. 😕
Ranguna commentedon May 17, 2020
Yeah, it'd be nice if I could find a way to make this work but I've spent the better part of the day yesterday trying to figure it out, alas I wasn't able to :(
I'll just keep casting the result of the mapper to
anyuntil typescript can properly type these situations.Thank you either way 👍
erjiang commentedon Jul 24, 2020
Just wanted to add a real-world example that I ran into (and I think falls under this bug):
Yes, this switch case could be expressed as a map in the simplest case, but not if there's programmatic logic involved.
jcalz commentedon May 3, 2024
cross-linking #33014, which seems to be the main issue tracking this (even though this is older)