Reducing Typescript and Redux Boilerplate with Partial Actions

Static types can help reduce defects in evolving codebases, but their benefits need to balance out against the inevitable boilerplate. We’ve previously used TypeScript to secure the Redux dispatch flow, but the repetition involved didn’t feel good at the time and hasn’t gotten any better with age.

The problem is that we want to use type-narrowing to reveal logical issues, while still being able to describe like actions in generic terms. Consider a family of actions representing the states of an API request:

// actions.ts
type ListRequest = {
  count?: number
  page?: number

type Books = {
  resource: string,
  items: Array<Book>

export type Action =
  { type: 'LIST_BOOKS_REQUEST', request: ListRequest }
| { type: 'LIST_BOOKS_RESPONSE', request: ListRequest, response: Books }
| { type: 'LIST_BOOKS_ERROR', request: ListRequest, error: string }

We should be able to count on strong, static guarantees anywhere we dispatch or consume the LIST_BOOKS actions.

// reducers/books.ts
import { Action } from '../actions'

type Books = {
  isLoading: boolean,
  error: null | string,
  books: Array<Book>,

const initialState = {
  books: [],
  isLoading: false,
  error: null,

function books (state: Books = initialState, action: Action): Books {
  switch (action.type) {
      return { ...state, isLoading: true }
    case 'LIST_BOOKS_ERROR':
      return { ...state, isLoading: false, error: action.error }
      return {
        error: null,
        isLoading: false,
        books: state.books.concat(action.response.items)
      return state

We would see a compiler error if books tried to access an unknown field. Good. But in order to create actions from the LIST_BOOKS family, we need to explicitly set both their types and their contents. In the previous attempt, we needed a fairly hefty definition to be able to represent asynchronous actions in a reasonably generic way:

const _listBooks: ApiAction<ListRequest, Books> = {
  request: (request) =>
    ({ type: 'LIST_BOOKS_REQUEST', request }),
  success: (response, request) =>
    ({ type: 'LIST_BOOKS_RESPONSE', request, response }),
  error: (error, request) =>
    ({ type: 'LIST_BOOKS_ERROR', request, error }),

function createApiAction<Q, S>(a: ApiAction<Q, S>, fetch: (q: Q) => Promise<S>) {
  return (req?: Q) => {
    // Make API request and handle result, dispatching
    // appropriate actions along the way

export const listBooks = createApiAction(_listBooks, opts => api.books.list(opts))

The ApiAction type enables a generic interface, but the type conversion underpinning it in _listBooks is a compromise. Since we can’t define type strings dynamically, every new API action family we define will need its own implementation. The common interface is better than working with bespoke actions. But it’s hardly a joy.

Partial Actions

The React ecosystem has trained us (quite rightly) to prize obviousness. Using discrete actions to represent changes within the application makes it clear–sometimes painfully clear–to follow what’s going on. We’re about to break all that.

The little bargain we’re about to strike will but save us a heap of boilerplate, but at the cost of discreet actions. We’ll lose a degree of obviousness in reducer logic and middleware. We’ll keep type-safety. Goethe can opine as he will.


Returning to our first example, let’s replace the three actions in the LIST_BOOKS family with a single type full of optional properties:

export type Action = {
  type: 'LIST_BOOKS',
  request?: ListRequest,
  response?: Books,
  error?: string,

We can think of this as a “partial” action. Like other forms of deferred state, the LIST_BOOKS action will be filled out with additional content as the request proceeds.

Remember that uncomfortable thing we were about to do? This is it. But using this pattern, we can now extract a generic type for the possible values of an API (or really any asynchronous) request:

type ApiAction<Q, S> = {
  request?: Q,
  response?: S,
  error?: string,

export type Action =
  ({ type: 'LIST_BOOKS' } & APIAction<ListRequest, Books>)

References to LIST_BOOKS elsewhere will still be narrowed to the correct type, but we can no longer refer to _ERROR and _SUCCESS actions representing the possible outcomes. Instead, we’ll need to derive state from the action’s content.

function books (prevState: Books = initialState, action: Action): Books {
  if (action.type === 'LIST_BOOKS') {
    const state = Object.assign({}, initialState)
    if (action.response) {
      state.books = state.books.concat(action.response.items)
    } else if (action.error) {
      state.error = action.error
    } else {
      state.isLoading = true
    return state
  return prevState


The upside? Reusing the type string across multiple states can save quite a bit of ceremony–in this example replacing the actions in a traditional TypeScript/redux implementation, boilerplate dropped by over 50%.

But whether it’s worth breaking the boundary between actions’ responsibilities will depend on the project and team. Partial actions are just a compromise between language and convention that may make sense in one context but not in another.

And as ever, let me know how it goes!