Search code examples
typescriptdiscriminated-unionunion-typesmapped-types

Type properties to discriminated union types


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.


Solution

  • 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