I've found it frequently desirable to be able to "look up" the union variant associated with a type tag. For example, given a union
type Foo = { tag: 'n'; val: number } | { tag: 's'; val: string }
one wants to be able to write a type operator
type Lookup<T extends Foo['tag']> = // ???
such that Lookup<'n'> = number and Lookup<'s'> = string.
I don't _think_ this is possible in the current type system (would welcome correction on that point). Instead, we can use the trick of _starting_ with a Lookup type, and deriving the union type from it:
type Lookup = {
n: number
s: string
}
type Foo = {
[T in keyof Lookup]: { tag: T; val: Lookup[T] }
}[keyof Lookup]
then the inferred type of Foo is
type Foo = {
tag: "n";
val: number;
} | {
tag: "s";
val: string;
}
as desired, and we have our lookup type: Lookup['n'] = number and Lookup['s'] = string.
So I use this pattern a lot, and I wanted to generalize it:
type Unionize<Lookup> = {
[T in keyof Lookup]: { tag: T; val: Lookup[T] }
}[keyof Lookup]
Unfortunately, this doesn't do what you want:
type Foo = Unionize<{
n: number
s: string
}>
Here, Foo is inferred to be
type Foo = {
tag: "n" | "s";
val: string | number;
}
This seems like a bug!
Looking at quickinfo on Unionize<Lookup>, I see:
type Unionize<Lookup> = { tag: keyof Lookup; val: Lookup[keyof Lookup]; }
which is "simplified" in the same surprising/incorrect way I see in #17908. I think these are the same underlying issue.
Workaround?
type Unionize<Lookup, X = { [T in keyof Lookup]: { tag: T; val: Lookup[T] } }> = X[keyof X]
@gcanti workaround is ingenious. The default forces type evaluation.
The first report of this issue seems to be https://github.com/Microsoft/TypeScript/issues/15756.
Wow, very cool @gcanti!
I didn't realize type parameter defaults could reference previous type parameters. This opens up a whole new world...
Using @gcanti's nifty type parameter default trick I put together a new typescript library, unionize, which allows generating tagged unions along with associated creation functions, predicates and match functions. Would welcome any feedback!
Just gotta say @gcanti's proposal is impressive :)
This seems like a bug!
This is the expected behavior. mapped types are a transformation on properties of a type, they do not generate a union type. Many operations that happen on mapped types e.g. inference, rely on the fact that they are homomorphic transformation.
@mhegazy: It may be expected behavior if you know how mapped types are implemented, but it's very surprising, as it violates the seemingly airtight principle of composition:
type X = ...
type F<T> = ...
type G<T> = ...
type FoG<T> = F<G<T>>
type FGX1 = F<G<X>>
type FGX2 = FoG<X>
// FGX1 should be equivalent to FGX2
Can you explain more about why generating a union type (i.e., T[A|B] → T[A]|T[B]) breaks a constraint about mapped types?
Thanks!
For completeness' sake/History, the workaround was first found by @nirendy / @tycho01 during their exploration in #12215 (ref https://github.com/Microsoft/TypeScript/issues/12215#issuecomment-307614501, https://github.com/Microsoft/TypeScript/issues/16018#issuecomment-307626787).
Is this really "working as intended" in light of #18042 as a fix to #15756 ?
unionize should be working now after #15756.
FYI to anyone who gets here, #21316 now allows you to look up a union type by tag value.
Most helpful comment
Workaround?