Tutorial: Getting Started with Redux and TypeScript

This is the first of several tutorials on building React/Redux applications with TypeScript. If you’re in a hurry, skip on over to the finished project on Github; everyone else, read on!


Redux is trending, and a number of applications I’ve encountered recently are built around its central dogma of pure functions and unidirectional dataflow. These projects grow with mixed results, but the ones with the happiest teams attached share certain similarities in their clear delineation between state in actions and stores; their well-documented central store; and the obvious relationships they establish between the store and its reducers.

First, some context. Redux shares a common motivation (and a diminished-but-not-entirely-eliminated volume of boilerplate) with Facebook’s flux architecture, but centralizes application state and adopts a functional approach to managing it. As Redux applications grow, however, a central, organically-evolving store can grow into a big problem.

In a weakly-typed language like JavaScript, it’s not always obvious what certain variables represent. That’s true for even the simplest objects, but take a reasonably complex one–the monolithic store at the heart of a large-ish web application, say–and saddle up those gumshoes for some serious sleuthing.

You already have a large-ish redux application? No problem. Its reducers can be decomposed to specific concerns and gradually extracted to separate applications. But recalling the central role of clarity in developer happiness, the biggest problem may not be the size of the application but simply understanding the shape and provenance of the state inside it.

Over the past few years, the browser-side ecosystem has grown a host of projects focused on improved type-safety for frontend applications. From Flow annotations through typed supersets like TypeScript or entirely new languages like Dart, strong types no longer need to be the second-class citizens of the browser.

Let’s take a look at the patterns that emerge as we formalize the types inside a Redux application. We can somewhat arbitrarily start with TypeScript, but similar approaches carry over to Flow, Dart, or any other flavors of the week. JavaScript works. Redux works with JavaScript. We’re not adding anything except formality–but in the right setting, with visibility on the line, that’s worth something.

If you’re just interested in the results, check out the finished counter over on github. Otherwise, it’s time to get to work.

Setting up

It would hardly be a JavaScript project if it began without at least a few dependencies. We’ll need the typescript compiler, webpack and the ts-loader for bundling, and redux for the application itself.

$ npm install -g typescript webpack
$ npm install --save-dev redux ts-loader

There’s also some initialization, as we need to tell the typescript compiler about the project:

$ tsc --init
message TS6071: Successfully created a tsconfig.json file.

Manually edit tsconfig.json to enable JSX and include the root typings definition:

{
  "compilerOptions": {
    "outDir": "./dist/",
    "sourceMap": true,
    "noImplicitAny": true,
    "module": "commonjs",
    "target": "ES5"
  }
}

We’re almost ready to roll. All that’s left is to configure the webpack build. There’s a detailed guide on this over in the typescript handbook, but just use the following webpack.config.js:

module.exports = {
  entry: './src/index.ts',
  output: {
    filename: './dist/bundle.js',
  },
  resolve: {
    extensions: ['', '.webpack.js', '.web.js', '.ts', '.js']
  },

  module: {
    loaders: [{ test: /\.ts$/, loader: 'ts-loader' }]
  }
};

The Counter Application

Phew, setup. Take a quick water-break, catch your breath, and when you’re back we’ll implement a small application. Counters are simple. Let’s do a counter. Press a button, watch a number increase–it’s hardly the definition of excitement, but illustration’s sake it will do.

Note that the webpack configuration above had made a couple of assumptions that we’ll need to meet:

  1. Files with the .ts extension will be pushed through TypeScript
  2. The project will be compiled from the src directory

In other words, we’ll wind up with a directory structure like this:

├── dist
└── src
    ├── actions
    │   └── index.ts
    ├── index.ts
    └── reducers
        └── index.ts

Actions

In Redux, actions are events that can update application state. Redux’s default typings describe actions in terms of a minimal inteface with a type (what the action is):

interface Action {
  type: any
}

Since our application will be well aware of each of its actions, however, we can redeclare Action as a union of explicit types that each implement redux’s interface:

export type Action = {
  type: 'INCREMENT_COUNTER',
  delta: number,
} | {
  type: 'RESET_COUNTER',
}

export const incrementCounter = (delta: number): Action => ({
  type: 'INCREMENT_COUNTER',
  delta,
})

export const resetCounter = (): Action => ({
  type: 'RESET_COUNTER',
})

There are several patterns of interest here. Note how:

  • We’re using type (rather than interface) to close Action for extension. It’s done.

  • Providing a string literal type for each action will enable checking (and, critically, narrowing) of possible action types elsewhere in the application. This will prove particularly useful when extracting action contents in our reducers.

  • Both the Action type and various action creators are declared and exported here. That’s fine for a simple application, but as its complexity increases it may make sense to move the various action creators to separately imported (read: maintained) modules.

Note also that the action creators are declared with more type information than we really need, which will be a recurring theme throughout this project. Even where types can be inferred, full definitions may help improve legibility and clarify intent. They’re a tool, not a hard requirement. If you’ve been here before, just ignore them.


Now that we have something together, let’s pause for just a moment to see TypeScript in action.

Since this code eventually needs to run in a browser, and since browsers know nothing about the strange type annotations now littering our code, the annotations will stick around only long enough for the type-checker to sign off. Let’s compile the action:

$ tsc src/actions/index.ts

We can now find the JavaScript output in src/actions/index.js:

"use strict";
exports.incrementCounter = function (delta) { return ({
    type: 'INCREMENT_COUNTER',
    delta: delta
}); };

Once our implementation has percolated through the compiler, it arrive back at JavaScript–and likely the same JavaScript we would have written if TypeScript weren’t in the mix at all. Everything added–types, interfaces, casts, and so on? That’s for our benefit.

But that’s just TypeScript. On to the store.

Store

This is why we’re here. Redux relies on a central store; leave it unattended, and the result trends towards an ominous black box. But by preemptively declaring what’s inside, we can avoid the mystery and enlist the type-checker in ensuring that it doesn’t creep back in.

Of course, the store behind our counter isn’t very complex:

export namespace Store {

  export type Counter = { value: number }

  export type All = {
    counter: Counter
  }
}

As with the actions, this structure belies a few deliberate decisions about the store. In particular, note that:

  • A single, finalized type–All in the example above–is exposed to describe the store’s complete structure. When a Redux application shares the store, it shares all of it. In cases where we need less state–the arguments to named reducers, for instance–we may only want a subset of the state. That’s fine; other types within the Store can be exposed as needed.

  • The Store occupies its own namespace, which could live in a top-level .d.ts declaration, or within a module somewhere near the reducers. Both work. It’s likely a good idea to import the Store while starting out to keep dependencies explicit, but as the application grows, it may be convenient to allow global, implicit references to the Store type.

  • All types in the store are declared within its namespace. Even though action payloads and component props may closely resemble data-structures that live within the store, as the application grows it will be clearer to define the store’s state separately from other, transient state within the application.

For all that, we haven’t really added anything new. We’ve just taken the data structures that would already exist inside a Redux application and laid out their shape in formal terms.

“But wait!” you’re thinking. “You haven’t even initialized the store! All we’ve got is a lousy type definition”. And you’re right: though it’s useful to look at its structure now, we won’t be able to implement the store until we’ve written a reducer.

Reducers

Now that we have actions and a store–or at least its shape–we can get ready to update it. Here’s a simple reducer that will consume the IncrementCounter action and update the counter accordingly:

import { Action } from '../actions'

const initialState: Store.Counter = {
  value: 0,
}

function counter (state: Store.Counter = initialState, action: Action): Store.Counter {
  const { value } = state
  switch (action.type) {
    case 'INCREMENT_COUNTER':
      const newValue = value + action.delta
      return { value: newValue }
    case 'RESET_COUNTER':
      return { value: 0 }
  }

  return state
}

This is standard Redux. Blink, and you could miss the modicum of type-safety that we’ve now sprinkled around the edges. We now expect the same type (Store.Counter) coming in and going out; we expect an Action corresponding to each change, and by switching on the action’s type TypeScript can determine the action format and allow us access to its content. This is a powerful business, as it allows the type system to verify that the action matches a known type:

case 'INCREMENT': // error: `type 'INCREMENT' is not comparable to type ...`
  // ...

As an added bonus, once the type is known, TypeScript can narrow the type definition to verify access to action contents:

case 'INCREMENT_COUNTER':
  const { delta } = action // OK!
  // ...
case 'RESET_COUNTER':
  const { value } = action // error: `Property 'value' does not exist...`
  // ...

Redux + TypeScript

In any case, we have a complete reducer and we’re finally ready to put it all together. Here’s how it looks:

import { createStore, store as ReduxStore } from 'redux'
import { reducers, Store } from './reducers'

const store: ReduxStore<Store.All> = createStore(reducers)

Ready to try it out? In index.ts, let’s set up a store and fire off some updates:

store.subscribe(() => {
  console.log(store.getState())
})

store.dispatch(incrementCounter(1)) // { counter: { value: 1 } }
store.dispatch(incrementCounter(1)) // { counter: { value: 2 } }
store.dispatch(incrementCounter(1)) // { counter: { value: 3 } }

It may not look like much, but from here on out the counter application will be working with statically-checked actions, reducers, and store. So far, we’ve seen structure and definitions of:

  • an application-specific Action generic, as well as a pattern for defining actions and their companion action creators that implement it

  • a shared Store containing the application state

  • a reducer consuming the actions to populate a portion of the store

Sound a bit like Redux?


Up next: this is the first part of a short series about bolstering Redux applications with TypeScript. In the next post, we’ll extend the application with a simple React UI.

Featured