lort
lort

Reputation: 1457

Turning discriminated union into object

Is there a way to turn a discriminated union like this:

type Something = {
   type: 'mode';
   values: 'first' | 'second';
} | {
   type: 'part';
   values: 'upper' | 'lower';
};

into

{
    mode: 'first' | 'second';
    part: 'upper' | 'lower';
}

using some generic type?

So far I tried something like this:

type MyUnion = {
   type: string;
   values: string;
};

type DiscUnionToObject<U extends MyUnion> = {
   [V in U['type']]: U['values']
}

but when I do DiscUnionToObject<Something> it produces

{
    mode: 'first' | 'second' | 'upper' | 'lower';
    part: 'first' | 'second' | 'upper' | 'lower';
}

I can't find a way for generic type to "understand" that 'upper' | 'lower' are not a part of Something when type is set to mode.

Upvotes: 1

Views: 450

Answers (2)

lort
lort

Reputation: 1457

Since Typescript 2.8 there is a possibility to "pluck out" a single components of discriminated unions:

Extract<P, { type: 'mode' }>;

And thanks to that, it is now possible to do exactly what was intended in the first place:

type DiscUnionToObject<U extends MyUnion> = {
    [V in U['type']]: Extract<U, { type: V }>['values'];
}

With this type, when we do DiscUnionToObject<Something>, we get what we wanted to:

{
    mode: 'first' | 'second';
    part: 'upper' | 'lower';
}

Upvotes: 1

jcalz
jcalz

Reputation: 327944

TypeScript lacks some type operators you'd need to do what you want. You'd like to be able to say something like:

type DiscUnionToObject<U extends MyUnion> = {
  [V in U['type']]: (U & { type: V })['values']
}

where the (U & { type: V }) intersection would pluck out a single element of the discriminated union. For example, if U is Something and V is part, then we're talking about (Something & { type: 'part' }) which is morally equivalent to {type: 'part', values: 'upper'|'lower'}, but the compiler does not recognize this: it would have to say that 'part'&'mode' is never, and that any object with a never-valued property is itself never, but neither of these reductions happen (well, not where we need it to).

So you can't do it that way. You'd also like to be able to iterate over unions and/or intersections and map each element to produce other unions and/or intersections, sort of a more general version of mapped types. But you can't do that either.


TypeScript is much better at programmatically producing discriminated unions than it is at programmatically analyzing them. So, depending on your use case, you might be able to do the reverse of what you're asking for. Start with the object and produce the union:

type SomethingObject = {
  mode: 'first' | 'second'
  part: 'upper' | 'lower'
}

type ObjectToDiscUnion<O, V = {
  [K in keyof O]: {type: K, values: O[K]}
}> = V[keyof V]

type Something = ObjectToDiscUnion<SomethingObject>;

You can verify that the Something above is the same as your original one. Hope that helps; good luck!

Upvotes: 2

Related Questions