17

I have a discriminated union type that differentiates types based on a string literal field. I would like to derive a mapped type that maps all of the types in the union to their corresponding discriminator literal values.

e.g.

export type Fetch = {
    type: 'fetch',
    dataType: string
};

export type Fetched<T> = {
    type: 'fetched',
    value: T
};

// union type discriminated on 'type' property
export type Action =
    | Fetch
    | Fetched<Product>;

// This produces a type 'fetch' | 'fetched'
// from the type 
type Actions = Action['type'];

// I want to produce a map type of the discriminator values to the types 
// comprising the union type but in an automated fashion similar to how I
// derived my Actions type.
// e.g.
type WhatIWant = {
    fetch: Fetch,
    fetched: Fetched<Product>
}

Is this possible in TypeScript?

bingles
  • 11,582
  • 10
  • 82
  • 93
  • Possibly relevant: [Get Type of Union By Discriminant](https://stackoverflow.com/questions/48750647/get-type-of-union-by-discriminant) – jcalz May 02 '18 at 01:17

1 Answers1

40

With the introduction of conditional types in TypeScript 2.8, you can define a type function which, given a discriminated union and the key and value of the discriminant, produces the single relevant constituent of the union:

type DiscriminateUnion<T, K extends keyof T, V extends T[K]> = 
  T extends Record<K, V> ? T : never

As of TypeScript 2.8, you can also use the built-in Extract utility type to simplify the above conditional:

type DiscriminateUnion<T, K extends keyof T, V extends T[K]> 
 = Extract<T, Record<K, V>>

And if you wanted to use that to build up a map, you can do that too:

type MapDiscriminatedUnion<T extends Record<K, string>, K extends keyof T> =
  { [V in T[K]]: DiscriminateUnion<T, K, V> };

So in your case,

type WhatIWant = MapDiscriminatedUnion<Action, 'type'>;

which, if you inspect it, is:

type WhatIWant = {
  fetch: {
    type: "fetch";
    dataType: string;
  };
  fetched: {
    type: "fetched";
    value: Product;
  };
}

as desired, I think. Hope that helps; good luck!

jcalz
  • 264,269
  • 27
  • 359
  • 360
  • A follow up to this: I've tried writing the same sort of `DiscriminateUnion` without using a generic union type -- that is, for a specific union type. It doesn't work, however, without specifying some `T extends MyUnionType`. Any idea why? A minimal example: ``` ``` – Ran Lottem May 04 '19 at 20:10
  • That example is so minimal there's nothing in it. Probably the answer to your question is that the technique uses a [distributive conditional type](https://github.com/Microsoft/TypeScript/wiki/What's-new-in-TypeScript#distributive-conditional-types) which only works if you are checking a bare generic type parameter. Of course you can use the generic version and make a new type alias which plugs in your specific type. If you need more help maybe you should ask a new question; comments on a year-old answer are not the best place to post code and get eyes on your issue. Good luck! – jcalz May 04 '19 at 23:53
  • This answer is amazing. Thank you – jbmilgrom Dec 03 '20 at 22:01
  • Wonderful answer, thank you! It turns out that TypeScript has a built-in conditional type for this bit: `T extends Record ? T : never`. It can be simplified to `type DiscriminateUnion = Extract>`. – Aleksandr Hovhannisyan Apr 09 '22 at 13:21