DEV Community

Cover image for How to add TypeScript types to useReducer
Akos
Akos

Posted on • Originally published at akoskm.com

How to add TypeScript types to useReducer

useReducer is a great way to abstract some of your application's complexity in React.
In this post, I'm going to show you how you can add TypeScript types to reducers and save yourself hours of debugging time! 🥳

As your application grows you start moving from useState to useReducer that's going to hide some of the complexity of your app. However, without the correct typings, you can misuse the dispatch function of useReducer and run into unexpected errors.

Imagine having a login form which state is managed by this reducer:

function authReducer(state, action) {
  switch (action.type) {
    case "success":
      return { success: true, username: action.value, error: "" }
    case "failure":
      return { success: false, username: "", error: action.value }
    default:
      throw Error()
  }
}
Enter fullscreen mode Exit fullscreen mode

The way how you would use this reducer in your React app is the following:

const [state, dispatch] = useReducer(authReducer)

function submit(data) {
  try {
    const response = await axios.post("/api/login", { data });
    dispatch({
      type: "success",
      value: response.data.user,
    });
  } catch (error) {
    dispatch({
      type: "failure",
      value: error.message,
    });
  } 
}
Enter fullscreen mode Exit fullscreen mode

This works, however, without using types nothing prevents you from accidentally doing:

dispatch({
  type: "failure",
  value: response.data.user,
})
Enter fullscreen mode Exit fullscreen mode

This is why I like to introduce types to my reducers as soon as possible so such errors are caught at compile time. Let's do that with our example:

type User = {
  id: string;
  username: string;
}

export type AuthState = {
  success: boolean;
  user: User | null;
  error: string;
}

export type AuthAction = {
  type: "success" | "failure";
  value: User | string;
}

export function authReducer(state: AuthState, action: AuthAction) {
  switch (action.type) {
    case "success":
      return { success: true, user: action.value, error: "" }
    case "failure":
      return { success: false, user: null, error: action.value }
    default: throw Error()
  }
}
Enter fullscreen mode Exit fullscreen mode

while we typed our reducer now, because of how we typed the action:

type AuthAction = {
  type: "success" | "failure";
  value: User | string;
}
Enter fullscreen mode Exit fullscreen mode

it's still possible to send the wrong value to the wrong type of action ie. an error message for the "success" type:

dispatch({
  type: "success",
  value: "Something went wrong",
})
Enter fullscreen mode Exit fullscreen mode

So let's be more specific about the specific value a type accepts:

type AuthAction =
  | {
    type: "success";
    value: User;
  }
  | {
    type: "failure";
    value: string;
  }
Enter fullscreen mode Exit fullscreen mode

Now let's type the reducer usage in our React app:

import { Reducer } from "react";
import { authReducer, authState, AuthState, AuthAction } from './auth_reducer';

const [state, dispatch] = useReducer<
  Reducer<AuthState, AuthAction>
>(authReducer, authState)

function submit(data) {
  try {
    const response = await axios.post("/api/login", { data });
    dispatch({
      type: "success",
      value: response.data.user,
    });
  } catch (error) {
    dispatch({
      type: "failure",
      value: error.message,
    });
  } 
}
Enter fullscreen mode Exit fullscreen mode

Now if you try to call the "success" type with an incorrect value:

dispatch({
  type: "success",
  value: "Something went wrong",
})
Enter fullscreen mode Exit fullscreen mode

You get the following error from TypeScript:

Type 'string' has no properties in common with type 'User'.
Enter fullscreen mode Exit fullscreen mode

Congratulations, you just added TypeScript types to your reducer, making it more resilient and straightforward to use! 🙌

Top comments (0)