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?
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}`
}