Follow

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use
Contact

How to narrow discriminated union based on Record key value

I have a union type whose elements have a discriminator property called "type". I then declare a Record<> whose keys are the "type" property values.

How do I declare the Record’s second generic parameter (Record< , this param>) such that values in the map/record are inferred based on the corresponding key?

TypeScript Playground

MEDevel.com: Open-source for Healthcare and Education

Collecting and validating open-source software for healthcare, education, enterprise, development, medical imaging, medical records, and digital pathology.

Visit Medevel

interface Dog {
  type: 'dog';
  breed: string;
}

interface Vehicle {
  type: 'vehicle';
  max_speed: number;
};

type Thing = Dog | Vehicle;

const printMap: Record<Thing['type'], (thing: Thing) => string> = {
  'dog': x => `Dog ${x.breed}`,
                    // ^^^^^ Property 'breed' does not exist on type 'Thing'. Property 'breed' does not exist on type 'Vehicle'.
                    // How do I make TypeScript infer `x` is a Dog because the key/property name is 'dog'?
  'vehicle': x => `Vehicle max spped ${x.max_speed}`
                                      // ^^^^^^^^^ Property 'max_speed' does not exist on type 'Thing'. Property 'max_speed' does not exist on type 'Dog'.
                                      // How do I make TypeScript infer `x` is a Vehicle because the key/property name is 'vehicle'?
}

>Solution :

You can’t use a Record<K, V> for this, since you need different types for each property value, but Record<K, V> has the same property type V for each key. Instead you need to build a mapped type that Extracts the relevant member of the discriminated union for each property. Possibly like this:

type DiscriminatedUnionHandler<T extends Record<K, PropertyKey>, K extends keyof T, R> =
  { [P in T[K]]: (foo: Extract<T, Record<K, P>>) => R }

Here the DiscriminatedUnionHandler<T, K, R> takes a discriminated union type T, its discriminant key name K, and the return type R of the individual handler functions. The resulting type has properties for each value of T[K] (which is "dog" | "vehicle" for Thing) and the corresponding property is a function that accepts the relevant member of T and returns R.

For Thing this looks like:

type DiscriminatedUnionHandler<T extends Record<K, PropertyKey>, K extends keyof T, R> =
  { [P in T[K]]: (foo: Extract<T, Record<K, P>>) => R }

interface Dog {
  type: 'dog';
  breed: string;
}
interface Vehicle {
  type: 'vehicle';
  max_speed: number;
};

type Thing = Dog | Vehicle;

const printMap: DiscriminatedUnionHandler<Thing, "type", string> = {
  'dog': x => `Dog ${x.breed}`,
  'vehicle': x => `Vehicle max spped ${x.max_speed}`
}

Playground link to code

Add a comment

Leave a Reply

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use

Discover more from Dev solutions

Subscribe now to keep reading and get access to the full archive.

Continue reading