October 14, 2020
/
Careers

Using Redux with Typescript for superior type safety

The benefits of Typescript in a team environment for building and maintaining production-worthy codebases are nothing new. At Cleo we committed early to developing our web and mobile apps with Typescript to give us the safety of static typing when developing our product.

One issue we faced building SPAs with Typescript was additional boilerplate when combined with other paradigms, specifically state management and Redux. The Redux docs[1] give a good starting point for Typescript integration, but their approach remains convoluted. We wanted a way of simplifying this integration to minimise the effort of achieving full type coverage within our codebase.

Our issue

  • Using Redux to manage state, but typescript can lead to additional boilerplate so type safety less likely to be used
  • Typescript shines when used everywhere in the codebase
  • Various state refactors and migrations (e.g. redux-thunk → redux-sagas) only benefit from typescript coverage when the underlying state is fully typed

Solution by example

We'll create a simple app for displaying a customer's transactions to show how we can create type safety alongside Redux.

State

We define a new type for our transaction, and the store's state.

// types.ts

type Transaction = {
 id: string;
 description: string;
 amount: number;
 categoryId: Category['id'];
};

type State = {
transactions: Transaction[];
};

Actions

We use the typesafe-actions library to generate typed actions. Defining a new params type for when we dispatch the action.

// actions.ts

import { createStandardAction } from 'typesafe-actions';
import { Transaction } from "../types";

type SetTransactionDescriptionParams = { transactionId: Transaction['id'], description: Transaction['description'] };
export const setTransactionDescription = createStandardAction('SET_TRANSACTION_DESCRIPTION')<SetTransactionDescriptionParams>();

export const deleteTransaction = createStandardAction('DELETE_TRANSACTION')<Transaction['id']>();

The following shows the type safety we get when (incorrectly) dispatching actions:

Reducers

Where typesafe-action starts to shine.

The dispatched action is typed with the ActionType helper. This builds a union type of the actions:

action: ActionType<typeof transactionListActions>

// evaluates to

{ type: 'SET_TRANSACTION_DESCRIPTION', payload: SetTransactionDescriptionParams } || { type: 'DELETE_TRANSACTION', payload: Transaction['id'] }

We use a switch statement on the action's type, and the getType helper to extract the action's type. This allows the payload's type to be correctly inferred & used within the scope.

// reducer.ts

import * as transactionListActions from './actions';
import { ActionType, getType } from 'typesafe-actions';
import { Category, Transaction } from "../types";

export const transactionListReducer = (state: TransactionListState, action: ActionType<typeof transactionListActions>): TransactionListState => {
 switch (action.type) {
   case getType(transactionListActions.setTransactionCategory): //getType evaluates to 'SET_TRANSACTION_DESCRIPTION'
// Payload is now typed correctly
     const { transactionId, categoryId } = action.payload;
...
   case getType(transactionListActions.deleteTransaction):
// Payload is now typed correctly
     const transactionId = action.payload;
...

   default:
     return state;
 }
};

The following shows the payload type suggestions we get when within a case.

Selectors

Simply type the state

// selectors.ts

import { TransactionListState } from "./reducer";
import { Transaction } from "../types";

export const selectTransactions = (state: TransactionListState) => state.transactions;
export const selectCategories = (state: TransactionListState) => state.categories;

export const selectTransaction = (state: TransactionListState, transactionId: Transaction['id'] ) =>
state.transactions.find(transaction => transaction.id === transactionId);

Alternatives & follow ups

  • Recently been a lot of talk about Redux Toolkit, could this help simplify further?
  • Do we even need Redux anymore? Next up we'll be talking about a Redux alternative using built-in React Hooks & Context API.

Read more

Careers

4 easy ways to get up to speed with a remote team

I joined Cleo in March as a Backend Ruby Engineer, three weeks after I joined the UK went into lockdown and I was working in a fully remote team. These four approaches to working have helped me stay an active contributor to the team over the last three months of isolation.

Thursday, July 23, 2020
Life at Cleo

Learning from failure - Postmortems at Cleo

It is a truth universally acknowledged that if you develop software, things will break and go wrong. Here at Cleo, we accept that this is a part of life — but it’s important to take the time to learn and improve as a result.

Friday, March 20, 2020
Seen enough?
Download Cleo
Screenshot of the chat screen and paycheck breakdown feature