eldarshamukhamedov
eldarshamukhamedov

Reputation: 505

What's the best way to infer the callback argument type from a union type in Typescript?

I am trying to write a helper that let's me subscribe to a specific type of dispatched actions in a React app. These are the action interfaces I'm working with:

enum ActionTypes {
  MoveToLine = 'MOVE_TO_LINE',
  MoveToColumn = 'MOVE_TO_COLUMN',
}

interface MoveToLineAction {
  type: ActionTypes.MoveToLine;
  payload: { line: number };
}

interface MoveToColumnAction {
  type: ActionTypes.MoveToColumn;
  payload: { column: number };
}

type Actions = MoveToLineAction | MoveToColumnAction;

And here's the implementation of subscribeToAction:

const subscribeToAction = <A extends { type: string }, T extends ActionTypes>(
  type: T,
  onAction: (a: A) => void,
) => (action: A) => {
  if (action.type === type) {
    onAction(action);
  }
};

I want to be able to use it like this:

// Subscribe to a specific type of action
subscribeToAction(ActionTypes.MoveToLine, action => {
  action.payload.line;
});

The problem I'm running into is that I want the type of action in the onAction listener to be inferred automatically. In the code above, I get a Property 'payload' does not exist on type '{ type: string; }' error. I can manually type the listener to get around this like this:

subscribeToAction(ActionTypes.MoveToLine, (action: MoveToLineAction) => {
  action.payload.line;
});

But it seems redundant to pass in an ActionType and then also have to specify the action type in the listener. Do you guys have any suggestions as to how to avoid doing this?

Upvotes: 2

Views: 454

Answers (1)

jcalz
jcalz

Reputation: 328282

Here's how I'd go about it:

type ActionOfType<T extends ActionTypes> = Extract<Actions, { type: T }>;

const subscribeToAction = <T extends ActionTypes>(
    type: T,
    onAction: (a: ActionOfType<T>) => void,
) => (action: Actions) => {
    if (action.type === type) {
        // assertion necessary or explicit type guard: 
        onAction(action as ActionOfType<T>);
    }
};

subscribeToAction(ActionTypes.MoveToLine,
    action => { // action inferred as MoveToLineAction
        action.payload.line;
    }
);

You don't really want two generic parameters in subscribeToAction(). You want T corresponding to the ActionTypes constituent passed in as type, and you want the argument to the onAction callback to automatically be constrained to the corresponding constituent of Actions. The ActionOfType<T> type function shows how you can do this. Oh, and the return value of subscribeToAction should be a function that takes any Actions, right? That's why you do the if (action.type === type) check.

Also note that because action is of type Actions, you need to tell the compiler that the test if (action.type === type) actually narrows action down to ActionOfType<T>. I just use a type assertion.

Okay, hope that helps you. Good luck!

Upvotes: 2

Related Questions