Adding Types to Reducer Functions in React using TypeScript


Borislav Hadzhiev

Last updated: Apr 21, 2021


Check out my new book

How to add Types to Reducer functions in React #

There are a couple of things that make typing reducer functions in react a little less error prone and give you some nice type checking and auto complete capabilities.

Among them are:

  • Using enums for action types
  • Using unions for actions
  • Being explicit abut the reducer's return value
  • Throw an error for the default case - if you didn't handle it, most likely it wasn't intended (even though we are using typescript and we should never mistype - the default case is almost never what you want)

Adding types to Reducer Functions - Example #

Let's look at a simple copy-paste style example in a single file called App.tsx and go over some of the key points:

import {useEffect, useReducer} from 'react'; type UserAttrs = { gender?: string; name?: string; country?: string; email?: string; }; type UserState = { loading: boolean; error: string | null; data: UserAttrs; }; enum ActionType { FETCH_USER = 'fetch_user', FETCH_USER_SUCCESS = 'fetch_user_success', FETCH_USER_ERROR = 'fetch_user_error', } type Action = | {type: ActionType.FETCH_USER} | { type: ActionType.FETCH_USER_SUCCESS; payload: Required<UserAttrs>; } | {type: ActionType.FETCH_USER_ERROR; payload: string}; function reducer(_state: UserState, action: Action): UserState { switch (action.type) { case ActionType.FETCH_USER: { return {loading: true, error: null, data: {}}; } case ActionType.FETCH_USER_SUCCESS: { return {loading: false, error: null, data: action.payload}; } case ActionType.FETCH_USER_ERROR: { return {loading: false, error: action.payload, data: {}}; } default: { throw new Error(`Unhandled action type - ${JSON.stringify(action)}`); } } } function App() { const [{loading, error, data}, dispatch] = useReducer(reducer, { loading: false, error: null, data: {}, }); useEffect(() => { const fetchUser = async () => { dispatch({type: ActionType.FETCH_USER}); try { const res = await fetch(''); const parsed = (await res.json()) as { results: [ { gender: string; name: {first: string}; location: {country: string}; email: string; }, ]; }; const user = parsed.results[0]; dispatch({ type: ActionType.FETCH_USER_SUCCESS, payload: { gender: user.gender, name:, country:, email:, }, }); } catch (error) { if (error instanceof Error) { dispatch({type: ActionType.FETCH_USER_ERROR, payload: error.message}); } else { dispatch({ type: ActionType.FETCH_USER_ERROR, payload: 'Something went wrong', }); } } }; fetchUser(); }, []); return ( <div> {loading && <h3>Loading...</h3>} {error && <h1 style={{color: 'red'}}>{error}</h1>} <h1> <pre>{JSON.stringify(data, null, 2)}</pre> </h1> </div> ); } export default App;

The first thing to look at is using an enum for the ActionType variables. You don't want to be duplicating action type strings at the case statements, nor in the dispatch calls, you could easily mistype an action and you don't get the nice autocomplete from typescript.

For our Action type we use a union. Typing the action as a union allows us to use the case statement like a type guard. In other words - if the action type is equal to ActionType.FETCH_USER_ERROR - you must provide a payload parameter of type string, otherwise typescript will error out.

In the default case of our reducer we simply throw an error, the default case is almost never what you want - it better to be explicit - if we dispatched an action which wasn't handled we should know about it - it's strange an not intuitive.

When dispatching actions always use the ActionType enum for autocompletion and to avoid typos.

Further Reading #

Use the search field on my Home Page to filter through my more than 3,000 articles.