Tutorial: Getting Started with Redux and TypeScript
- 8/16/2016
- ·
- #howto
- #typescript
- #redux
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:
- Files with the
.ts
extension will be pushed through TypeScript - 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 thaninterface
) to closeAction
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 theStore
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 theStore
while starting out to keep dependencies explicit, but as the application grows, it may be convenient to allow global, implicit references to theStore
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 ita shared
Store
containing the application statea 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.