Type-Safe Redux Reducers
Tags: typescript, redux, webdev, programming
Published: 2019-10-17 05:32:21

---
title: Type-Safe Redux Reducers
description: The main problem that has to be solved is how to show TypeScript what the concrete action type is after checking the `type` property of Redux actions.
published: true
tags: typescript, redux, webdev, programming
canonical_url: https://www.domysee.com/blogposts/typescript-redux
---

The main problem that has to be solved is how to show TypeScript what the concrete action type is after checking the `type` property of Redux actions.

## Straightforward Solution

The most straightforward way to do this is creating a type for every action and using [type guards](https://www.typescriptlang.org/docs/handbook/advanced-types.html#type-guards-and-differentiating-types) to distinguish them.

Type guards are boolean functions that check if a parameter is a specific type, indicated by the return value being `parameter is Type`.  
Used in a condition, TS assumes the variable passed to the type guard is of the type the guard checks for. Only in the true branch of course.

An example solution with type guards:

```typescript
interface Action {
  type: string;
}

interface StartAction extends Action {
  date: Date;
}

interface SuccessAction extends Action {
  data: number;
}

interface ErrorAction extends Action {
  error: string;
}

export const createStart: () => StartAction = () => ({
  type: 'Start',
  date: new Date()
});

export const createSuccess = (data: number): SuccessAction => ({
  type: 'Success',
  data
});

export const createError = (error: string): ErrorAction => ({
  type: 'Error',
  error
});

// TYPE GUARDS
const isStartAction = (action: Action): action is StartAction => 
  action.type === 'Start';

const isSuccessAction = (action: Action): action is SuccessAction => 
  action.type === 'Success';

const isErrorAction = (action: Action): action is ErrorAction => 
  action.type === 'Error';

export interface State {
  lastStarted?: Date;
  data?: number;
  error?: string;
}

const defaultState: State = {
  lastStarted: undefined,
  data: undefined,
  error: undefined
};

const reducer = (state = defaultState, action: Action) => {
  if (isStartAction(action)) {
    return {
      lastStarted: action.date,
      data: undefined,
      error: undefined
    };
  }

  if (isSuccessAction(action)) {
    return {
      data: action.data,
      error: undefined
    };
  }

  if (isErrorAction(action)) {
    return {
      data: undefined,
      error: action.error
    };
  }

  return state;
};
```

This works, but is quite verbose. 

For every action, it's necessary to define

* The action type
* The action creator
* The type guard

And whenever an action changes, a property is added/removed/changed, it has to be adapted on 2 places, the specific action interface and the action creator.

It's possible to get rid of both, but that requires a little detour first.

## Inferring Concrete Types from Unions

TS has a neat type inference feature. Under specific circumstances, it can infer the concrete type from a union. What I mean is this

```typescript
const x: A | B = someObject;
if (someCondition) {
  // x is of type A here
}
```

For this to work `A` and `B` must have a property in common. In Redux actions it would be `type`. This is necessary so that TS knows this property can be accessed in the condition.

Additionally the `type` property must be a literal type, for example a literal string (e.g. `'Start'`) or number (e.g. `123`).  
This type is checked in the condition. Since each type's `type` property can only be a specific value, TS can infer the type based on the given value.

```typescript
interface A {
  type: 'Start';
  propA: number;
}

interface B {
  type: 'End';
  propB: number;
}

if (x.type === 'Start') {
  // x is of type A here
}
```

For this reason it's necessary that the `type` properties are literal types. If they were a generic `string`, there'd be nothing to go on for type inference. 

## Using Unions to Infer the Action Type

Armed with that knowledge, it's possible to get rid of the type guards, in exchange for adding `ActionTypes`, which is a union of all action types handled in that reducer.

```typescript
interface StartAction {
  type: 'Start';
  date: Date;
}

interface SuccessAction {
  type: 'Success';
  data: number;
}

interface ErrorAction {
  type: 'Error';
  error: string;
}

type ActionTypes = StartAction | SuccessAction | ErrorAction;

export const createStart = (): StartAction => ({
  type: 'Start',
  date: new Date();
});

export const createSuccess = (data: number): SuccessAction => ({
  type: 'Success',
  data
});

export const createError = (error: string): ErrorAction => ({
  type: 'Error',
  error
});

export interface State {
  lastStarted?: Date;
  data?: number;
  error?: string;
}

const defaultState: State = {
  data: undefined,
  error: undefined
};

export default (state = defaultState, action: ActionTypes) => {
  if (action.type === 'Start') {
    return {
      lastStarted: action.date,
      data: undefined,
      error: undefined
    };
  }

  if (action.type === 'Success') {
    return {
      data: action.data,
      error: undefined
    };
  }

  if (action.type === 'Error') {
    return {
      data: undefined,
      error: action.error
    };
  }

  return state;
};
```

## Removing Action Types with `ReturnType`

The last improvement to arrive at the final example is automatically inferring action types based on the return value of action creators.

This can be done with the helper type `ReturnType`.  
As the name says, it is a generic type that, if passed a function, returns the type of the returned value. For example

```typescript
type F = (p1: string, p2: number) => boolean;
type T = ReturnType<F>;
// T = boolean
```

*If you're interested how it works, check out my other blogpost [List of Built-In Helper Types in TypeScript](https://www.domysee.com/blogposts/ts-builtin-types-list).*

By using that helper type it's possible to create the union type directly from the return types of the action creators.

One thing to notice here is that the returned objects define the `type` like this: `'Start' as 'Start'`.  
This is so the inferred type is of the literal string (`'Start'`), because by default, when assigning a string, TS infers it to be of type `string`.

```typescript
type ActionTypes =
  | ReturnType<typeof createStart>
  | ReturnType<typeof createSuccess>
  | ReturnType<typeof createError>;

export const createStart = () => ({
  type: 'Start' as 'Start',
  date: new Date();
});

export const createSuccess = (data: number) => ({
  type: 'Success' as 'Success',
  data
});

export const createError = (error: string) => ({
  type: 'Error' as 'Error',
  error
});

export interface State {
  lastStarted?: Date;
  data?: number;
  error?: string;
}

const defaultState: State = {
  data: undefined,
  error: undefined
};

export default (state = defaultState, action: ActionTypes) => {
  if (action.type === 'Start') {
    return {
      lastStarted: action.date,
      data: undefined,
      error: undefined
    };
  }

  if (action.type === 'Success') {
    return {
      data: action.data,
      error: undefined
    };
  }

  if (action.type === 'Error') {
    return {
      data: undefined,
      error: action.error
    };
  }

  return state;
};
```

This is the final example, as concise and with the least typing effort possible.

All solutions are valid though, and depending on the rest of the codebase, the team and your preference you might want to choose a more explicit (first) way to implement type-safe reducers.