Type properties to discriminated union types

1.4k views Asked by At

I'd like to transform a type to a discriminated union type:

type Current = {
  A : number,
  B : string,
}

to

type Target= {
  type: 'A',
  value: number
}
| {
  type: 'B',
  value: string
}

So that I can discriminate the result.

function handle(result: ToTarget<Current>) {
 switch(result.type){
  case 'A':
   return result.value // number
  case 'B':
   return result.value // string
 } 
}

The closest I got was:

type ToTargetA<U> = { code: keyof U, value : U[keyof U] } 
// code and value are not in sync so code is 'A | B' and value is 'number | string' but discriminating on code does not narrow value.

type ToTargetB<U, K extends keyof U = any> = { code: keyof K, value : U[K] }
// requires me to specify K as a generic parameter which is not what I want.

I tried several conditional type expressions but couldn't get much closer.

2

There are 2 answers

1
jcalz On BEST ANSWER

Here's one way to do it:

type ToDiscriminatedUnion<T, KK extends PropertyKey, VK extends PropertyKey> =
  { [K in keyof T]: { [P in KK | VK]: P extends KK ? K : T[K] } }[keyof T];

You can verify that it produces the type you want:

type Target = ToDiscriminatedUnion<Current, 'type', 'value'>;
/* type Target = {
    type: "A";
    value: number;
} | {
    type: "B";
    value: string;
} */

The approach here is to build a mapped type with the same keys K as in your original object type T, but whose values are the {type: T, value: T[K]} types you want in the discriminated union. This type ends up becoming {A: {type: "A", value: number}, B: {type: "B". value: string}}.

Then we can look up the union of its property values by indexing into it with [keyof T], producing the desired {type: "A", value: string} | {type: "B", value: number} discriminated union.


The only extra thing I did there was to make it so you could specify what keys to give to the original key names (KK, being "type" in your case) and the original value names (VK, being "value" in your case). If you don't want to change that ever, you can hardcode it:

type ToDiscriminatedUnion<T> =
  { [K in keyof T]: { type: K, value: T[K] } }[keyof T];

type Target = ToDiscriminatedUnion<Current>;

Playground link to code

0
nathanwinder On

This works, but wondering if there is a cleaner/better solution.

type DistributedProperties<T> = { [P in keyof T] : { code: P, value: T[P]} } 
type Union<T> = DistributedProperties<T>[keyof DistributedProperties<T>]