Generating strongly typed reducer actions for React
Tags: react, typescript, usereducer, mappedtypes
Published: 2021-09-30 17:13:25

---
title: Generating strongly typed reducer actions for React
published: true
description: Using simple string action types with any payload value in React's reducer functions is highly error prone. Why not use Typescript's Mapped Types and Template Literals to generate a strongly typed set containing the valid type of payload for each action type.
tags: React, TypeScript, useReducer, MappedTypes
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/91yggydlz2rc7qyc72u7.jpeg
main_image: ![Shiny new reducers and gears](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/91yggydlz2rc7qyc72u7.jpeg)
---
Using reducers in the React Context api leaves a lot of room for error, even when using TypeScript. A reducer takes a single argument and returns a state. The common format for the argument is an object with an `action` property and a `payload` or `value` property. The `action`'s value is generally a string such as `setTitle`. In the case of `action:'setTitle'`, the `value` would then be the title you want to set on the state object. The problems with this approach are:
1. You have to look back at the reducer to see what the correct spelling and valid values of action are.
2. There is no type checking of the values that are linked with the action.

_This article assumes that you have a basic familiarity with React, `useReducer`, and TypeScript._

_If you just want to skip to the code, you can see the final working version on [this TS Playground](https://www.typescriptlang.org/play?target=6&q=373#code/JYWwDg9gTgLgBAJQKYEMDGMA0cDecDCEAdjEgB5ZxpSqmEnmUCuAzksgCZNpJRwC+cAGZQIIOACIa6GBIDcAWABQy8pFhwYATzBI4ASVIgAyjBSk4AXlzK4duMA4AuOCxhRgRAOaZb9mMAwADZILm4e3r5K9nAcSCzUwGABxAD8Ye6ePn52AI5MKCSBWi5ETCAARrzK-MrK2rpwxkgwpFAsADwAKgB8VjbR9gDaAAoORHAA1khaEEJwXXAoLHAABmwwACQ4+ChggShBwABeSB3hWXAAZHAjPfyrALoAtOkLo481dUoNeoZIJhabRW1marV4nX+JjMpB631+cAAghhgMQWABZPb9HA5OBDYzjKYzOYGIxg4GPFw4wYxOwyVFEFzGKK0+wAN0OTFCpIB5Ih+M+NIEXxUSjU0HgCKhyJSRBBSJRaMxYCG01m8xlDIxe0FyjQaPggQBnG4vH6AAo3OZuVDTNbsPTiC5pYq5QBKODOskwvSWPrUmIsADugTQAAtzY6iAA6KMegOsqjLPQAcg2+g4KacuMTcBoMCYUAmOGjpatpGwjhcUejHKCXP4iiFMTQybgaZaXUCISzOcT+cLxdL0fLSGwAWC3JrdYbTdzrbY7Y2ABF4olkgze83+y1B7hh6PsHEEh4N06lq7a5ykI2+3YF6mNgBFApFbRb3MxAdF-dln3YfJCgnEoL1lK96xvOdEziIQUCYIIYGzbdWX1OUIBCaNeFEKBzVWTViDgElthrKNBGAFYiAgeAWCYMB1FIDhVjdKD7FqZtvwmUcmzY75PDaWCeAMEZRDZRxeGEiAwBWBNw2AIIOBoIh0kKLQRWUPjeAEv56FICgJKkgY7CNEAvV5f8-A4ciwHMcMXEjV1TJAfD3SsPo2QgRw1KUVC3B5EAdMYfpqFoJAAooc0cEEZYDDCmB9JYD1otijp9Fi+K4VFIQmCIV0-Ik0S4hwnBZPkxT+GdfKxKgeK3Rkg08WMo8rJssNHn6Vh2CQLgeBw4yTR67BIqWFZbR9N0-B8+BUN0mAAHkKgAK2xRrYmamBwzYuwOLgc1cRSoxYujSrCrgGckEsYriBm+aFv4DLWWKsM5IUpAiE2+wOgAeihQ7jt4e64HGpQeLFMh6NwH6rsYbAoT+qB+CAA)_

The optimal solution would allow us to define the state object and its valid values, then TypeScript would provide code completion and type validation for the actions and values passed to the reducer function. A less optimal solution would be to do something similar to Redux and create a series of action functions that are strongly typed and wrap calls to the reducer. Then in your code you only use action functions and never call the reducer directly. However, TypeScript's mapped types can make that extra layer unnecessary.

Let's take a simple state type

```ts
type ItemState = {
    id: string,
    title: string,
    description?: string,
    quantity: number
}
```

We want to create a reducer that knows that `setId` and `setQuantity` are valid actions but `setID` and `setQty` are not. It should also type check so that the value paired with `setQuantity` is a number and not a Date or string.

The first step is to create a generic utility type called `Setters` that takes another type and produces "setPropertyName" style function names for each property on that type.

```ts
type Setters<T> = {
    [P in keyof T as `set${Capitalize<string & P>}`]-?: T[P]
}
```

`Capitalize` is a built in string modifier that capitalizes the passed string. The `-?` means that we remove the optional attribute of any property. `T[P]` gives us the type of each property of the passed in generic type.

We then use the Setters utility to generate a setters type for our state type.

```ts
type ItemSetters = Setters<ItemState>
/* 
This is equivalent to writing out:
type ItemSetters = {
    setId: string,
    setTitle: string,
    setDescription: string,
    setQuantity: number
}
*/
```

Now let's use another mapped type to create a map of action objects with valid action names and value types.
 
```ts
type ActionsMap = {
    [S in keyof ItemSetters]: {
        action: S,
        value: ItemSetters[S]
    }
}
/* 
This results in:
type ActionsMap = {
    setId: {
        action: 'setId',
        value: string
    }, ...
    setQuantity: {
        action: 'setQuantity',
        value: number
    }
}
*/
```

Now we need to extract the action/value objects out of ActionsMap and use the union of those objects as our action type in our reducer.

```ts
type ItemActions = ActionsMap[keyof ActionsMap]

const itemReducer = (state: ItemState, action: ItemActions) : ItemState => { return state }
```

The `itemReducer` doesn't actually do anything yet. We need to fill it out with the reducer logic. What you can't see here in the code sample is the ease of doing this. With the strongly typed `ItemActions` we will have code completion on the switch case statements we will write and type validation when we use the reducer. 

Here is the filled out reducer function:

```ts
const itemReducer = (state: ItemState, action: ItemActions) : ItemState => {
    switch(action.action) {
        case 'setId':
            return {...state, id: action.value};
        case 'setTitle':
            return {...state, title: action.value};
        case 'setDescription':
            return {...state, description: action.value};
        case 'setQuantity':
            return {...state, quantity: action.value};
        default:
            console.error(`Action of ${action.action} is not supported`);
     }
     return state;
}
```

Unfortunately our `default` statement has an error:

>Property 'action' does not exist on type 'never'.

That error occurs because we covered all possible valid cases. In order to account for a possible error, we can add a dummy action type. ItemActions becomes:

```ts
type ItemActions = 
    ActionsMap[keyof ActionsMap] | {action: 'other'}
```

Using the mapped types with string template literals approach really shows its power when you need to add another property to the state. For example, let's add a boolean 'backordered' property. We just have to add 3 total lines of code.

```ts
type ItemState = {
    ...
    quantity: number,
    backordered: boolean
}

...
   
    switch(action.action) {
    ...
    case 'setQuantity':
        return {...state, quantity: action.value};
    case 'setBackordered':
        return {...state, backordered: action.value};
    ...
```

Even better than the developer experience when adding or removing properties from state, is the experience of using the reducer. While it can't easily be shown in code snippets, the auto-complete and type validation is a game changer. No longer do you have to look back at the reducer to determine the correct spelling and what exactly type of value it is expecting. For example:

```ts
/* GOOD */
...
    dispatch({
        action: 'setQuantity',
        value: 5
    })
...

/* ERROR */
...
    dispatch({
        action: 'setQuantity',
        value: 'none'
   })
...
```

Hope this approach helps you when creating and using reducers. Once I figured it out, it has saved me plenty of development time and frustration.

Here is the final TypeScript annotated reducer:

```ts
type ItemState = {
    id: string,
    title: string,
    description?: string,
    quantity: number,
    backordered: boolean,
}

type Setters<T> = {
    [P in keyof T as `set${Capitalize<string & P>}`]-?: T[P]
}

type ItemSetters = Setters<ItemState>

type ActionsMap = {
    [S in keyof ItemSetters]: {
        action: S,
        value: ItemSetters[S]
    }
}

type ItemActions = 
    ActionsMap[keyof ActionsMap] | {action: 'other'}

const itemReducer = (state: ItemState, action: ItemActions) : ItemState => {
    switch(action.action) {
        case 'setId':
            return {...state, id: action.value};
        case 'setTitle':
            return {...state, title: action.value};
        case 'setDescription':
            return {...state, description: action.value};
        case 'setQuantity':
            return {...state, quantity: action.value};
        case 'setBackordered':
            return {...state, backordered: action.value};
        default:
            console.error(`Action of ${action.action} is not supported`);
     }
     return state;
}
```