Home > Software engineering >  Trouble with TypeScript generics
Trouble with TypeScript generics

Time:10-01

Here is the code:

type Args<State, ActionHandlers, Helpers> = {
    state: State,
    actionHandlers: ActionHandlers,
    helpers?: Helpers
}

type GenericActionHandler<State, Payload> = (state: State, payload: Payload) => Partial<State> | void

type GenericDispatch<State, ActionHandlers> = <
    Key extends keyof ActionHandlers,
    Handler extends ActionHandlers[Key],
    Payload = Handler extends GenericActionHandler<State, infer P> ? P : never
>(type: Key, ...payload: Payload extends undefined ? [] : [Payload]) => void

type GenericHelper<State, ActionHandlers, Payload> = (dispatch: GenericDispatch<State, ActionHandlers>, payload: Payload) => Promise<void> | void

type HelpersRemapped<State, ActionHandlers, Helpers> = {
    [Key in keyof Helpers]: <
        Helper extends Helpers[Key],
        Payload = Helper extends GenericHelper<State, ActionHandlers, infer P> ? P : never 
    >(payload: Payload) => Promise<void> | void
}

const create = <

    State extends {},

    ActionHandlers extends Record<string, GenericActionHandler<State, any>>,

    Helpers extends Record<string, GenericHelper<State, ActionHandlers, any>>

>({ state, actionHandlers, helpers }: Args<State, ActionHandlers, Helpers>) => {

    const dispatch: GenericDispatch<State, ActionHandlers> = (type, ...payload) => { console.log(type, [payload]) };

    type Remap = HelpersRemapped<State, ActionHandlers, Helpers>;

    const helpersRemapped: Partial<Remap> = {}
    for (const key in helpers) {
        helpersRemapped[key] = (payload) => helpers[key](dispatch, payload);
    }

    return {
        dispatch,
        helpers: helpersRemapped as Remap
    };
}

let { dispatch, helpers } = create({
    state: {
        msg: "hi",
        num: 123,
        flag: true
    },
    actionHandlers: {
        SET_MSG: (state, msg: string) => ({ msg }),
        SET_NUM: (state, num: number) => ({ num }),
        SET_FLAG: (state, flag: boolean) => ({ flag })
    },
    helpers: {
        setFlagAsync: async (dispatch, flag: boolean) => { // I don't understand what the problem is here :'(
            await Promise.resolve();
            dispatch("SET_FLAG", flag);
        }
    }
});

helpers.setFlagAsync(true);
dispatch("SET_MSG", "test");

Playground Link

If you click through to the TS Playground link above the problem is underlined in red squiggle on line 61 when trying to define a helper method on the helpers object.

Type '(dispatch: GenericDispatch<{ msg: string; num: number; flag: boolean; }, Record<string, GenericActionHandler<{ msg: string; num: number; flag: boolean; }, any>>>, flag: boolean) => Promise<...>' is not assignable to type 'GenericHelper<{ msg: string; num: number; flag: boolean; }, { SET_MSG: (state: { msg: string; num: number; flag: boolean; }, msg: string) => { msg: string; }; SET_NUM: (state: { msg: string; num: number; flag: boolean; }, num: number) => { ...; }; SET_FLAG: (state: { ...; }, flag: boolean) => { ...; }; }, any>'.
  Types of parameters 'dispatch' and 'dispatch' are incompatible.
    Types of parameters 'type' and 'type' are incompatible.
      Type 'Key' is not assignable to type '"SET_MSG" | "SET_NUM" | "SET_FLAG"'.
        Type 'string' is not assignable to type '"SET_MSG" | "SET_NUM" | "SET_FLAG"'.
          Type 'Key' is not assignable to type '"SET_FLAG"'.
            Type 'string' is not assignable to type '"SET_FLAG"'.(2322)

If you force the code to run it works fine but TypeScript is not satisfied and will not compile. I've been banging my head on this error for the better part of two days and haven't made any progress. Any help is greatly appreciated!

PS - I know, this is similar to what Redux already does. Sadly I do not have control.

CodePudding user response:

You define actionHandlers inline within your create function. As you've written your type definition, we expect the keys of the helpers to be Key extends keyof ActionHandlers. TypeScript is casting that specifically as one of the key names you defined in the actionHandlers, but you are passing "SET_FLAG", which is being interpereted as a string. A string is too wide a definition to statisfy the extends keyof ActionHandlers type. In other words, Key extends keyof ActionHandlers is too narrow to be satisfied by a string type, so typescript errors. When I've run into cases like this, I always think "can't typescript recognize that the value is indeed a keyof the object??". I guess not.

In any case, if you define actionHandlers before feeding it to create, ans cast it as const, it solves the problem:

const state = {
    msg: "hi",
    num: 123,
    flag: true
}

const actionHandlers = {
    SET_MSG: (stateArg: typeof state, msg: string) => ({ msg }),
    SET_NUM: (stateArg: typeof state, num: number) => ({ num }),
    SET_FLAG: (stateArg: typeof state, flag: boolean) => ({ flag })
} as const;

// Invoking the create function.
let { dispatch, helpers } = create({
    state,
    actionHandlers,
    helpers: {
        setFlagAsync: async (dispatch, flag: boolean) => {
            await Promise.resolve();
            dispatch("SET_FLAG", flag);
        }
    }
});

Working typescript playground

Unfortunately when moving the declaration of the argument outside of the function call, we lose the typing on the state variable, so I had to explicitly type it.

Edit

As I struggling to fall asleep last night, I ended up realizing there's a quicker way from your original code to a solution. Just use as const inline, like this:

let { dispatch, helpers } = create({
    state: {
        msg: "hi",
        num: 123,
        flag: true
    },
    actionHandlers: {
        SET_MSG: (state, msg: string) => ({ msg }),
        SET_NUM: (state, num: number) => ({ num }),
        SET_FLAG: (state, flag: boolean) => ({ flag })
    } as const, // <---------------------------------------- here
    helpers: {
        setFlagAsync: async (dispatch, flag: boolean) => { 
            await Promise.resolve();
            dispatch("SET_FLAG", flag);
        }
    }
});

That way you can indeed define your function arguments within the function call.

Works great

  • Related