Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Using TypeScript With Redux

DZone's Guide to

Using TypeScript With Redux

In this article, we will explore some sample annotations and develop a standard boilerplate for use with a single Redux store.

· Web Dev Zone
Free Resource

Get deep insight into Node.js applications with real-time metrics, CPU profiling, and heap snapshots with N|Solid from NodeSource. Learn more.

This article is featured in the new DZone Guide to Web Development. Get your free copy for more insightful articles, industry statistics, and more!

Redux is a library that provides a way to manage application state in one place. Not every application needs help with this, but for many applications, it can simplify access to this global state without causing incidental complexity by restricting the ways it can be updated and accessed.

Redux is all well and good by itself, but using TypeScript, we can add type annotations to greatly increase the type-safety of Redux-based React apps. In this article, we will explore some sample annotations and develop a standard boilerplate for use with a single Redux store, and then expand this solution to cover a multi-store Redux configuration, demonstrating along the way the benefits of this added type safety when writing reducers and dispatching actions.

Dummy-Proofing Redux

Let’s look at Redux’s “Getting Started” example:

import { createStore } from ‘Redux’
function counter(state = 0, action) {
 switch (action.type) {
 case ‘INCREMENT’:
 return state + 1
 case ‘DECREMENT’:
 return state - 1
 default:
 return state
 }
}
const store = createStore(counter)

The interesting part here is the reducer function counter. The signature of this function is critical: it accepts a state (of any shape), an action (which must have a “type” field), and returns a state of the same shape as the original. Get this wrong and your program can break in subtle ways. If only there was some way to enforce this signature top revent this. We can start by defining the shape of our state:

type AppState = number

Extremely boring stuff. Next, we’ll define our action, which is a bit more interesting. Redux considers any object with a “type” key a valid action, but we can do much better than that by restricting our actions down to only ones we expect:

interface IncrementAction {
 type: ‘INCREMENT’
}
interface DecrementAction {
 type: ‘DECREMENT’
}
type AppAction = IncrementAction | DecrementAction

That | is the secret sauce of this whole article and represents one of my favorite TypeScript features: union types. You can read it as “or”: The action is either an Increment or a Decrement. Now, we need simply to apply these types to our function signature:

function counter(state: AppState = 0, action: AppAction):
AppState {
 switch (action.type) {
 case ‘INCREMENT’:
 return state + 1
 case ‘DECREMENT’:
 return state - 1
 default:
 return state
 }
}

Besides annotating the arguments and return values, we’ve not had to change the function’s implementation at all; TypeScript’s type inference does an excellent job checking the above without requiring extra annotations. However, if we do something like misspell “INCREMENT” or forget to return the state in the default: case, the type checker will tell us right away, because we’ve provided it a list of the only acceptable values for action.type.

Unfortunately, we don’t automatically get this benefit when calling store.dispatch — by default, it will (again) accept any object with a “type” key. There are two ways around this: 1) You can manually annotate store.dispatch when you call it:

store.dispatch<AppAction>({type: ‘INCREMENT’})

2) You can create your own Dispatch type and apply it to store.dispatch via an intermediate variable:

type AppDispatch = (action: AppAction) => AppAction
const dispatch: AppDispatch = store.dispatch
dispatch({type: ‘INCREMENT’})

This latter method is particularly useful when using the connect utility from react-Redux.

Multi-Store Applications

The above is well and good, but eventually most applications will find themselves separating AppState and the reducer into multiple reducers. Each of these reducers will have to provide:

  • Its own state

  • Its own actions

  • Its own reducer function

I also like to include a fourth item: a function returning a blank state. This will come in handy frequently.

Let’s move our counter reducer into its own file (I usually put these all together in a reducers/directory). (Let’s also change the state to store the count as a key, just to make it more typical of a real-life Redux state object).

One more detail: to be correctly typed, the reducer function should accept any action, not just the ones from its reducer.

So, we’ll need to import AppAction from the store. Luckily, TypeScript can deal with this sort of partial circular import.

// reducers/counter.ts
import { AppAction } from ‘../store’
export interface State {
 count: number
}
export const blankState = () => ({
 count: 0
})
interface IncrementAction {
 type: ‘INCREMENT’
}
interface DecrementAction {
 type: ‘DECREMENT’
}
export type Action = IncrementAction | DecrementAction
export function reducer(state: State = blankState(),
action: AppAction): State {
 switch (action.type) {
 case ‘INCREMENT’:
 return { count: state.count + 1}
 case ‘DECREMENT’:
 return { count: state.count - 1}
 default:
 return state
 }
}
Now, in store.ts, we’ll connect this sub-reducer to our
global store:
import { createStore, combineReducers } from ‘Redux’
import * as counter from ‘./reducers/counter’
export type AppState = {
 counter: counter.State
}
export type AppAction = counter.Action
export type AppDispatch = (action: AppAction) => AppAction
const blankAppState = (): AppState => ({
 counter: counter.blankState()
})
const store = createStore(
 combineReducers({
 counter: counter.reducer
 }),
 blankAppState()
)

From this, it should be clear how we might extend our store with additional reducers. Imagine that we wrote a “todos”reducer as well. After connecting it to our store, our store.tsfile might look like:

import { createStore, combineReducers } from ‘Redux’
import * as counter from ‘./reducers/counter’
import * as todos from ‘./reducers/todos’
type AppState = {
 counter: counter.State,
 todos: todos.State
}
type AppAction = counter.Action | todos.Action
const blankAppState = (): AppState => ({
 counter: counter.blankState(),
 todos: todos.blankState()
})
const store = createStore(
 combineReducers({
 counter: counter.reducer,
 todos: todos.reducer
 }),
 blankAppState()
)

So, what have we gained?

Firstly, our reducers are now strongly typed, right down to their accepted actions — no sign of an any type anywhere, nor is it possible to forget to return the state.

In addition, dispatch functions will only accept valid actions (provided that they have been annotated as described above). I find that this obviates the need for a separate action generating function as many Redux guides recommend. We could, of course, provide such a function:

const incrementAction = (): IncrementAction => ({
 type: ‘INCREMENT’
}

...but, since actions are type-checked for validity anyhow thanks to our efforts, it becomes little extra effort to construct the action at the time that you dispatch it. Your editor will catch any errors as you code!

Typing React-Redux

With our state and dispatch types, we can make a React integration that is much more type-safe as well. When writing a component with providers — as we do when we use React-Redux’s connect — I like to split its props into parts based on who provides them.

import * as React from ‘react’
interface StateProps {
 count: number
}
interface DispatchProps {
 increment: () => void
 decrement: () => void
}
class MyComponent extends React.PureComponent<StateProps &
DispatchProps, {}> {
 // ...
}
This will let us annotate the functions that we’ll be
providing to connect:
import { connect } from ‘react-Redux’
import { AppState, AppDispatch } from ‘./store’
const MyComponentWrapper = connect(
 (state: AppState): StateProps => ({
 count: state.counter.count
 }),
 (dispatch: AppDispatch): DispatchProps => ({
 increment: () => dispatch({type: ‘INCREMENT’}),
 decrement: () => dispatch({type: ‘DECREMENT’}),
 })
)(MyComponent)

Let’s dissect the above:

  • The state provider is annotated to accept AppState and return StateProps, giving us that extra type-checking.

  • The dispatch provider is similarly annotated to accept AppDispatch and return DispatchProps. UsingAppDispatch ensures that our calls to dispatch can only be made with valid messages — if we misspell a type or miss an argument, the compiler (or our editor tooling) will catch it and tell us.

Summary

Although Redux comes with its own types for the above, they lean heavily on TypeScript’s any type. By providing our own implementations of the State, Action, and Dispatch types we can gain vastly increased type safety.

This article is featured in the new DZone Guide to Web Development. Get your free copy for more insightful articles, industry statistics, and more!

Node.js application metrics sent directly to any statsd-compliant system. Get N|Solid

Topics:
web dev ,redux ,react ,web application development ,typescript

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}