Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 561
i9or Avatar asked Mar 03 '20 17:03

i9or


People also ask

What is the usereducer hook in react?

The useReducer hook accepts a reducer type (state, action) => newState and returns a state object paired with a dispatch method much like Redux. Now the official useReducer documentation will show you how to define a reducer that accepts actions you will call with a dispatch method.

What is typescript typescript in react hooks?

TypeScript lets you type-check your code in order to make it more robust and understandable. In this guide, I will show you how to set up TypeScript types on React hooks (useState, useContext, useCallback, and so on). Let's dive in.

How does typescript handle state in usereducer?

In addition, TypeScript ensures that the initial state (the second parameter of useReducer) is a full State object. Finally, when we call dispatch, it must be a valid Action object. There’s no need to use a constant for the 'add_todo' type because it must match one of the valid Action types.

How does usereducer know the state of the action object?

So it knows state is State and that dispatch is a function that can only dispatch Action objects. In addition, TypeScript ensures that the initial state (the second parameter of useReducer) is a full State object. Finally, when we call dispatch, it must be a valid Action object.


2 Answers

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.

like image 186
i9or Avatar answered Nov 15 '22 05:11

i9or


The approach looks ok, the problem is that your reducer has a return type of string but if it is passed a UserActionWithoutPayload then it might return action.username where username is undefined.

So one way to fix it would be to relax your return types:

function userReducer(state: string, action: UserAction): string | undefined {
  switch (action.type) {
    case "LOGIN":
      return action.username;
    case "LOGOUT":
      return "";
    default:
      throw new Error("Unknown 'user' action");
  }
}
like image 45
Will Jenkins Avatar answered Nov 15 '22 03:11

Will Jenkins