bingles
bingles

Reputation: 12223

TypeScript: General switchExpression function for discriminated unions

I'm trying to make a general purpose switch expression function that can take any discriminated union type (for simplicity using a type property as the discriminator) and a map of its discriminator values to callback functions and return the result of the appropriate callback.

e.g.

type One = {
  type: 'one',
  numeric: number
};

type Two = {
  type: 'two',
  text: string
};

type Union = One | Two;

const union: Union = ... // some appropriate assignment

// The function switchExp should be aware of what the map should 
// look like based on the type of its first arg. The argument
// passed to each callback should be properly discriminated based
// on the key in the map.
let result: number | string = switchExp(union, {
  one: u => u.numeric, // compiler should know that u is of type One
  two: u => u.text // compiler should know that u is of type Two
});

Upvotes: 2

Views: 102

Answers (1)

Titian Cernicova-Dragomir
Titian Cernicova-Dragomir

Reputation: 250106

We can use a mapped type and the conditional type ReturnValue to get the desired effect. There is a hitch however in the way we can infer the types for the function parameters. If we try to do it in a single function call the parameters will be typed as any.

So for example this will not work as expected:

function switchExp2<T extends { type: string }, R extends { [P in (T["type"]]: (v: Extract<T, { type: P }>) => any }>(u: T, o: R): ReturnType<R[keyof R]> {
    return null as any;

}

let result2 = switchExp2(union, {
    one: u => u.numeric, // u is implictly tyed as any
    two: u => u.text // u is implictly tyed as any
});

The compiler tries to infer T from all possible sites and just gives up instead of getting to a conclusion. The simple solution is to fix T first and then have a second call for the maping object:

function switchExp<T extends { type: string }>(u: T) {
    return function <R extends { [P in T["type"]]: (v: Extract<T, { type: P }>) => any }>(o: R): ReturnType<R[keyof R]> {
        return null as any; // replace with reasonable implementation 
    }
}

let result: number | string = switchExp(union)({
    one: u => u.numeric, //u is of type One
    two: u => u.text //  u is of type Two
});

Upvotes: 3

Related Questions