Search code examples
typescripttypescript-typingstypescript-genericsreact-typescript

How would you suggest to merge interfaces with same properties but different value types


I'm using useReducer on ReactJS and I've defined a couple of action interfaces to use them in my reducer function. I see that I use the same properties, but sometimes different payload values.

interface MembersAction {
  type: "JOIN" | "LEAVE";
  payload: string;
}

interface MessagesAction {
  type: "MESSAGE";
  payload: Message;
}

interface ErrorAction {
  type: "ERROR";
  payload: string;
}

interface RoomAction {
  type: "ROOM";
  payload: RoomData;
}

To be honest it feels like bloat and I feel like there must be a better way to achieve what I'm doing.

Here's the reducer function:

type Action = MembersAction | MessagesAction | RoomAction | ErrorAction;

const reducer = (state: RoomState, action: Action) => {
  switch (action.type) {
    case "JOIN":
      return {
        ...state,
        members: [...state.members, action.payload],
      };
    case "LEAVE":
      return {
        ...state,
        members: state.members.splice(state.members.indexOf(action.payload), 1),
      };
    case "MESSAGE":
      return {
        ...state,
        messages: [...state.messages, action.payload],
      };
    case "ROOM":
      return action.payload;
    case "ERROR":
      return state.error
        ? {
            ...state,
          }
        : {
            ...state,
            error: action.payload,
          };
    default:
      return state;
  }
};

Solution

  • Your code looks fine to me, and seems to behave well in this playground link.

    • You've expressed this correctly as a discriminated union, such that TypeScript is inferring your payload type correctly in the switch block.
    • Your payload type is not compatible between your different Actions, so there isn't much to be gained here by treating payload polymorphically.
    • Your reducer approximates the visitor pattern, though Typescript's structural typing means you avoid some of the interfaces commonly associated with that pattern. If your reducer expresses an operation that is intrinsically something an Action should be able to do to itself, I can see the value in refactoring so every Action supplies an implementation of a method that works like your reducer does, but that accepts some class definition boilerplate instead of your switch statement boilerplate.

    When thinking through this, decide whether it is more likely that you'll be adding Actions that need to express a new behavior (such that the behavior should be encapsulated in an Action), or adding self-contained functions that operate on most or all new actions (such that the behavior should be encapsulated in a switch like you have). If it's unclear, leave it—personally I'd gladly accept code as clearly-defined as you have it.