i9or
i9or

Reputation: 2448

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.

Upvotes: 12

Views: 11312

Answers (4)

i9or
i9or

Reputation: 2448

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.

Upvotes: 20

Richard Matsen
Richard Matsen

Reputation: 23463

You need to give the specific reducer case some more type info by type-casting the action

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

type UserActionWithoutPayload = {
  type: string;
};

type UserAction = UserActionWithPayload | UserActionWithoutPayload;

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

let state = "";
state = userReducer(state, { type: "LOGIN", username: "John" });
console.log("LOGIN", state);

state = userReducer(state, { type: "LOGOUT" });
console.log("LOGOUT", state);

Playground Link

Upvotes: -1

Will Jenkins
Will Jenkins

Reputation: 9787

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");
  }
}

Upvotes: 1

James Lin
James Lin

Reputation: 182

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

In this function, you defined return type as a string. But when you pass down UserAction as UserActionWithoutPayload type, the return value should be undefined. These causes differ between your userReducer(...) function return type and real return type as a string and undefined. To fix this, you can change return type as any.

I hope this answer might be helpful in your work.

Regards. Lin.

Upvotes: 1

Related Questions