TypeScript and Async Redux Actions

This is the third of several short posts about building Redux applications with TypeScript. Part one introduced a simple counter application, which we then dressed up with a simple React UI. If you’re in a hurry, skip on over to the finished project on Github; everyone else, read on!

We’ve previously put TypeScript together with Redux, then added a toy React application on top of it. What we haven’t considered yet is the world outside. Even toys, however, may need to store their state for safekeeping and recall at a later date. If you’re just looking to see this in action, check out the example project over on github. For the play-by-play, though, read on!


Our Redux application won’t be making direct calls to the API. Instead, we’ll dispatch actions to indicate that certain conditions have happened:

  • an asynchronous API call has been requested (X_REQUEST)
  • an asynchronous API call has succeeded (X_SUCCESS)
  • an asynchronous API call has failed (X_ERROR)

So far, we’re straight straight out of the excellent redux documentation. The only tricky question is, “how should we represent these actions in TypeScript?” And with TypeScript 2.0, we’re edging closer to a satisfying answer: “as a discriminate union.”

Here’s the idea. When the same string literal is present across multiple types, TypeScript can use it to narrow down the shape of the type:

type Foo = { type: 'FOO', str: string }
type Bar = { type: 'BAR': num: number }

type Action = Foo | Bar

function handle (a: Action): string {
  switch (a.type) {
    case 'FOO':
      return a.str + ' is a string!'
    case 'BAR':
      return a.num.toString() + 'is a number!'

The catch–and it’s a big one–is that string literals mean quite a bit of boilerplate upfront. We need to explicitly declare the set of actions (one string literal), we won’t be able to set up dynamic (generic) action creators, and we need to rewrite the string any time it turns up in a loop or conditional expression.

There will probably be ways around this in the future. There may be ways around it now (and if you have one, I would love to hear from you). But for the time being we’re going to be doling out some redundant code.

In return for all of that cutting and pasting (incidentally, something that IDEs are really good at for–we’ll gain reasonable static guarantees throughout the async action flow.

Anyway, to the actions themselves. Now that we’re using discriminant unions, all actions will be attached to a single type. Call it Action. Since we’ve already touched on the three events generally involved with asynchronous actions, we can extend the union describing our actions so far with their implementations.

export type Action =

// UI actions
     delta: number }
|  { type: 'RESET_COUNTER' }

// Async actions...
| ({ type: 'SAVE_COUNT_REQUEST',
     request: { value: number } })
| ({ type: 'SAVE_COUNT_SUCCESS',
     request: { value: number },
     response: {})
| ({ type: 'SAVE_COUNT_ERROR',
     request: { value: number },
     error: Error })

There’s a general structure here that we’ll keep for all “groups” of actions describing asynchronous events.

  • every action includes a request field describing the original request
  • success actions include a response field to hold the asynchronous result
  • error actions include an error field to hold any errors that arise

It may make sense to structure these another way, depending on preference and application, but they should be consistent. As we’ll see in a moment, homogeneity here will simplify matters in other parts of the application.

Before we get there, note that we’ve already built up some of the boilerplate I promised. To keep things tidy as the list of actions grows, we can alias commonly-used types and use intersections to compose them.

type Q<T> = { request: T }
type S<T> = { response: T }
type E = { error: Error }

Here, Q<T> expresses actions containing requests, S<T> expresses those with responses, and E those that contain errors.

We can then add a few aliases for reused types of requests and responses.

type QEmpty = Q<null>
type QValue = Q<{ value: number }>

Here are the SAVE_COUNT_X actions rewritten using the tighter-if-marginally-more-opaque aliases. And since we’ll need them in a moment anyway, here are some additional actions (LOAD_COUNT_X) for comparison’s sake.

export type Action =
// ...
| ({ type: 'SAVE_COUNT_REQUEST' } & QValue)
| ({ type: 'SAVE_COUNT_SUCCESS' } & QValue & S<{}>)
| ({ type: 'SAVE_COUNT_ERROR'   } & QValue & E)

| ({ type: 'LOAD_COUNT_REQUEST' } & QEmpty)
| ({ type: 'LOAD_COUNT_SUCCESS' } & QEmpty & S<{ value: number }>)
| ({ type: 'LOAD_COUNT_ERROR'   } & QEmpty & E)

Async Action Creators

There’s an obvious relationship between the SAVE_COUNT_X actions, but we haven’t yet made it explicit to the type system. Let’s fix that.

export type ApiActionGroup<_Q, _S> = {
  request: (q?: _Q)         => Action & Q<_Q>
  success: (s: _S, q?: _Q)  => Action & Q<_Q> & S<_S>
  error: (e: Error, q?: _Q) => Action & Q<_Q> & E

export const saveCount: ApiActionGroup<{ value: number }, {}> = {
  request: (request) =>
    ({ type: 'SAVE_COUNT_REQUEST', request }),
  success: (response, request) =>
    ({ type: 'SAVE_COUNT_SUCCESS', request, response }),
  error: (error, request) =>
    ({ type: 'SAVE_COUNT_ERROR',   request, error }),

export const loadCount: ApiActionGroup<null, { value: number }> = {
  request: (request) =>
    ({ type: 'LOAD_COUNT_REQUEST', request: null }),
  success: (response, request) =>
    ({ type: 'LOAD_COUNT_SUCCESS', request: null, response }),
  error: (error, request) =>
    ({ type: 'LOAD_COUNT_ERROR',   request: null, error }),

Now, when we need to refer to reference async actions, we can find them conveniently grouped within the saveCount and loadCount action groups.

We’re unfortunately packing on even more boilerplate. The action creators must return defined Actions using explicit string-literal types. We could conceivably work around this using dynamic action creators and a few generic-type hijinks, but as the code here is relatively simple and cut-and-paste operations are relatively cheap it may not be worth the trouble.


Our API will normally exist outside the redux application, or at least be the subject of a parallel design conversation. The persistence API for the counter has no surprises, however, so we’ve put off building it until now. We can use localStorage to achieve per-session persistence but still expose the same sort of promise-based API we might use to fetch data from a REST API or GraphQL server.

// ./api.ts
export const api = {

  // Save counter state
  save: (counter: { value: number }): Promise<null> => {
    localStorage.setItem('__counterValue', counter.value.toString())
    return Promise.resolve(null)

  // Load counter state
  load: (): Promise<{ value: number }> => {
    const value = parseInt(localStorage.getItem('__counterValue'), 10)
    return Promise.resolve({ value })

We’re leaning pretty heavily on the counter’s “toy” status to excuse the lack of validation, error handling, and formal request/response types. But, taking the focus back in the redux application, we now have something to call.


Remember that the core application only dispatches actions, never interacting directly with the API? That’s a job for middleware. This is where we’ll finally see the awesomeness of those discriminant unions at work–and even make some API requests while we’re at it.

We’ll start with the code:

// ./middleware/index.ts
import * as redux from 'redux'

import { api } from '../api'

import {
} from '../actions'

export const apiMiddleware = ({ dispatch }: redux.MiddlewareAPI<any>) =>
  (next: redux.Dispatch<any>) =>
    (action: Action) => {
      switch (action.type) {
        case 'SAVE_COUNT_REQUEST':
            .then(() => dispatch(saveCount.success({}, action.request)))
            .catch((e) => dispatch(saveCount.error(e, action.request)))

        case 'LOAD_COUNT_REQUEST':
            .then(({ value }) => dispatch(loadCount.success({ value }, action.request)))
            .catch((e) => dispatch(loadCount.error(e, action.request)))

      return next(action)

This switch can be dressed up quite a bit; it may even go away entirely. We could build up a map of action groups to api methods, for instance, and iterate over them each time the middleware is called. But it helps to illustrate some of the magic that all of our legwork has been building towards.

Before, if we wanted to extract an action’s payload, we would need to switch on its type and then assert its type:

// No longer needed!

type SaveCountRequestAction = {
  request: { value: number }

// ...
  api.save({ value: (action as SaveCountRequestAction).request.value })
    // ...

This required us to re-establish the association between the action’s type and various other fields: redundant, intuitively unnecessary, and prone to fat fingers.

But using the union type, we can now reference action.request without a type assertion! Because both SAVE_COUNT_REQUEST and LOAD_COUNT_REQUEST actions do have .request fields, and TypeScript is able to narrow the set of matching types in each case based on its type, it also recognizes the shape of the corresponding action. If we wanted to, we could even extract the { value } attached to SAVE_COUNT_REQUEST without complaint from the compiler:

  const { value } = action.request
  api.save({ value })
    // ...


From there on out, things go back to normal. We map the actions’ dispatches to components, add reducers to update state, and map changes back to the components. The hum-drum is over in the example project.

But we’ve seen some pretty good stuff! We’ve set up asynchronous actions, and–in exchange for some boilerplate–gained reasonable static assurance that both the middleware and the core application have a handle on their shape and structure. The counter’s still just a toy, but the same strategies outlined here can be (in fact are being) used in much more sophisticated applications. Not a bad day’s work.

The completed counter project is available for reference on github, build, code, tests, and all. And I’m looking forward to your suggestions, experiences, and feedback over on twitter.

Up next: We’re now ready to shore things up with Jest-powered unit tests. Read on!

Let’s keep in touch

Reach out on Twitter or subscribe for (very) occasional updates.

Hey, I'm RJ: sometimes-writer and intermittent micropoet. Broadcasts live from the Rose City.