Home > front end >  Infer typings in higher order function using keys of an object
Infer typings in higher order function using keys of an object

Time:06-07

I'm trying to create a higher order function which recevies an object of functions. Each function receives a state and an action object which has two fields (payload can be optional). The object must contain functions named as the field type of an action:

type RequiredActionFields<P = {}> = {
  type: string;
  payload: P;
};

Here's the function implementation:

type SomeFunction<S, A> = (state: S, action: A) => void;

type ObjWithFunctions<S, A extends RequiredActionFields> = Record<
  A["type"],
  SomeFunction<S, A>
>;

const functionThatDoesThings = <S>(state: S, cb: (draft: S) => void) => {
  // does domethings else
  return state;
};

function createThing<S, A extends RequiredActionFields>(
  functions: ObjWithFunctions<S, A>
) {
  return (state: S, action: A) => {
    const reducer = functions[action.type as keyof typeof functions];

    if (!reducer) {
      throw new Error(`Unhandled action type`);
    }

    const nextState = functionThatDoesThings(state, (draft) => {
      reducer(draft, action);
    });

    return nextState;
  };
}

And now I want to use this function like this:

type Action =
  | { type: "something"; payload: { id: 1 } }
  | { type: "another"; payload: { close: true } };

type State = {
  entities: [];
  open: boolean;
};

createThing<State, Action>({
  something: (state, action) => {
    // ERROR: I want this typing to be inferred automatically because of the object key 'something'
    action.payload.id;

    if (action.type === "something") {
      // WORKING: like this
      action.payload.id;
    }
  },
  another: (state, action) => {
    // ERROR: I want this typing to be inferred automatically
    action.payload.close;
  }
});

The problem I have is narrowing the type down to the function specified into the obejct of functions.

It's kind of hard to clearly explain my intent so I hope that the code shows what I'm trying to do. Here's a codesandbox to also try it out: https://codesandbox.io/s/nice-hermann-kiigqy?file=/src/index.ts:804-1325

CodePudding user response:

A mapped type will help here. Try replacing your type ObjWithFunctions = ... definition with this:

type ObjWithFunctions<S, A extends RequiredActionFields> = {
  [T in A['type']]: SomeFunction<S, A & { type: T }>
};

This creates a type with properties as follows: for each of the possible union values T in A['type'] (i.e. T = 'something' and T = 'another'), create a property with key T and value of type SomeFunction<S, A & { type: T }>. (In other words, a more flexible version of Record.)

The purpose of the A & { type: T } is to select out just the "branch" of your Action union that has the correct type.

  • Related