Search code examples
typescripttypescript2.0typescript3.0

Extracting type from union of types based on a discriminator


In the example below, how can I provide the correct typing for action argument in withoutSwitchReducer?

enum ActionTypesEnum {
    FOO = 'FOO',
    BAR = 'BAR',
}

type ActionTypes = {
    type: ActionTypesEnum.FOO,
    payload: { username: string }
} | {
    type: ActionTypesEnum.BAR,
    payload: { password: string },
};

// "withSwitchReducer" works fine as TS can infer the descriminator from action.type    

function withSwitchReducer(action: ActionTypes) {
    switch (action.type) {
        case 'FOO':
            return action.payload.username;
        case 'BAR':
            return action.payload.password;
        default:
            return null;
    }
}

// The code below gives as error as "action.payload.username" is not available on "action.payload.password" and vice versa

const withoutSwitchReducer = {
    [ActionTypesEnum.FOO]: (action: ActionTypes) => {
        return action.payload.username;
    },
    [ActionTypesEnum.BAR]: (action: ActionTypes) => {
        return action.payload.password;
    }
};

Same code with Intellisense here: TS Playground Link


Solution

  • There are two ways to do this.

    You can declare the type once:

    const withoutSwitchReducer: { [k in ActionTypesEnum]: (action: Extract<ActionTypes, { type: k }>) => string } = {
        [ActionTypesEnum.FOO]: (action) => {
            return action.payload.username;
        },
        [ActionTypesEnum.BAR]: (action) => {
            return action.payload.password;
        },
    };
    

    Or you can decribe them individually:

    const withoutSwitchReducer2 = {
        [ActionTypesEnum.FOO]: (action: Extract<ActionTypes, { type: ActionTypesEnum.FOO }>) => {
            return action.payload.username;
        },
        [ActionTypesEnum.BAR]: (action: Extract<ActionTypes, { type: ActionTypesEnum.BAR }>) => {
            return action.payload.password;
        },
    };
    

    The benefit of declaring the type once is obviously you don't have to do the same thing over and over, but describe them individually allows you to benefit from inferring the return type of those functions.

    UPDATE: as Titian Cernicova-Dragomir mentioned in the comment, you can declare it as a type to be reused elsewhere:

    type ReducerMap<T extends { type: string }> = { [P in T['type']]: (action: Extract<T, { type: P }>) => any }
    

    The return type of the function is any. I tried to find a way to infer that to the actual return value of each definition but that is not likely possible.

    And since it is used as a reducer map, you probably won't care about the return value as it is consumed by the framework.