reactjstypescripttypesreact-hooks

How to create type definition for the React useReducer hook action in Typescript?


Let's say we have userReducer defined like this:

function userReducer(state: string, action: UserAction): string {
  switch (action.type) {
    case "LOGIN":
      return action.username;
    case "LOGOUT":
      return "";
    default:
      throw new Error("Unknown 'user' action");
  }
}

What's the best way to define UserAction type so it will be possible to call dispatch both with username payload and without:

dispatch({ type: "LOGIN", username: "Joe"}});
/* ... */
dispatch({ type: "LOGOUT" });

If type is defined like this:

type UserActionWithPayload = {
  type: string;
  username: string;
};

type UserActionWithoutPayload = {
  type: string;
};

export type UserAction = UserActionWithPayload | UserActionWithoutPayload;

Compiler throws and error in reducer in the "LOGIN" case: TS2339: Property 'username' does not exist on type 'UserAction'.   Property 'username' does not exist on type 'UserActionWithoutPayload'.

If type is defined with optional member:

export type UserAction = {
  type: string;
  username?: string;
}

Then compiler shows this error: TS2322: Type 'string | undefined' is not assignable to type 'string'.   Type 'undefined' is not assignable to type 'string'.

What's missing here? Maybe the whole approach is wrong?

Project uses TypeScript 3.8.3 and React.js 16.13.0.


Solution

  • After hours of digging and experimenting found quite an elegant solution via Typescript enum and union types for actions:

    enum UserActionType {
      LOGIN = "LOGIN",
      LOGOUT = "LOGOUT"
    }
    
    type UserState = string;
    
    type UserAction =
    | { type: UserActionType.LOGIN; username: string }
    | { type: UserActionType.LOGOUT }
    
    function userReducer(state: UserState, action: UserAction): string {
      switch (action.type) {
        case UserActionType.LOGIN:
          return action.username;
        case UserActionType.LOGOUT:
          return "";
        default:
          throw new Error();
      }
    }
    
    function App() {
      const [user, userDispatch] = useReducer(userReducer, "");
    
      function handleLogin() {
        userDispatch({ type: UserActionType.LOGIN, username: "Joe" });
      }
    
      function handleLogout() {
        userDispatch({ type: UserActionType.LOGOUT });
      }
    
      /* ... */
    }
    

    No errors or warnings using approach above, plus there is a quite strict contract for action usage.