Redux is a perfect candidate for Typescript. As an app gets larger, with more actions and more complex reducers, it is all too easy to lose the structure of the root state and to pass inappropriate data through action payloads. Typescript adds a compile time guard against falling into reducer hell.
Unfortunately, to use typescript straight out of the box with Redux, all the reducers and actions must be explicitly given types that agree with each other. All this type duplication can just introduce a new place for error and discrepancy.
In a recent project, our team developed a set of utility functions and complementary types and interfaces that allow for strong typing in Redux with a minimum of type and interface duplication.
The pattern has the following benefits:
- Strongly Typed Redux - No more throwing around the wrong payload.
- Only define an action payload's type once - The type is then inferred in all interested reducers, action creators, and calling components.
- Defined Root State - Strongly typed state with easily referenceable structure.
To illustrate the pattern and how to use it, I'll walk through a trivial Redux app that implements strong typing. I'll assume familiarity with React/Redux architecture as well as a webpack build system, but if you'd like a introduction to that, check out this post.
If, instead, you'd prefer to skip my prosaic meanderings, you can check out the example code here on github.
Our app will be a trivial masterpiece consisting of two components, user_buttons
and flash_message
. The former will render two buttons that allow the user to dispatch an action and flash_message
will subscribe to the state and display the content of a fairly worthless message.
This walkthrough will follow this outline.
- File Structure
- Typescript Setup
- Actions
- Reducers
- Wiring Components
Throughout, I'll introduce how to implement a variety of utility functions that will allow our application to easily make use of Typescript's goodness. I'll focus on the essential helper functions, but will omit some code sections and interfaces for brevity. Check the example code repo to fill in any holes.
Project Structure
Our base directory structure is as follows:
├── actions
├── app
├── interfaces
├── reducers
└── utils
The actions
and reducers
directories will contain their eponymous redux logic. The app
directory holds our components and interfaces
has all interfaces required through our application. The utils
folder will house the specific functions and interfaces that our team developed to allow our project to easily make use of strong typing. I'll go into more detail of the file structure in the following sections.
Typescript Setup
Setting up Typescript with your React project consists of two steps: adding the correct loader to Webpack and downloading typings files for third party libraries.
Webpack
To set up Typescript with Webpack, make use of the ts-loader npm package for all .ts
and .tsx
files. Then, add in a tsconfig.json to manage your Typescript settings. I also made use of the webpack-dev-server to easily run our app. Checkout the code repo for an example.
Typings
Conveniently enough, there are a number of high quality type declaration files available so that we can use third party modules with Typescript without having to write the damn things ourselves. Typings is a package similar to npm
that allows us to manage our type declaration files. Install typings
as a dev-dependency, and then run the rather unfriendly typings install dt~<libary name> --global --save
. This will grab the typings file for you, and save it to typings.json
(a package.json
equivalent). Grab the typings files for all the external libraries and typings will make their interfaces globally available in your project.
Actions
In our project, we named action files by the section of the application they manipulate. Our action directory for this example will look like this:
actions
└── user_buttons
├── action_types.ts
└── actions.ts
In our case, only user_buttons
is concerned with dispatching actions, but this pattern would be repeated for other sections in larger apps. We'll begin by creating a map of our action constants strings in action_types.ts
. By making use of the createActionConstantsMap
and actionType<T>
helper functions, we'll be able to create a map of action constants that have properly typed payloads. This is the core of the strong typing around our Redux state and its actions.
I'll illustrate the use of both helper functions, and then show what they're doing under the hood.
Our app will have one action, SEND_FLASH_MESSAGE
which will have a payload of type FlashMessage
. The FlashMessage
type will be an object with message
and className
properties.
// action_types.ts
import { FlashMessage } from '../../interfaces/flash_message.interface';
import { actionType, createActionConstantsMap } from '../../utils/redux';
export default createActionConstantsMap('userButtons', {
SEND_FLASH_MESSAGE: actionType<FlashMessage>()
});
The actionType<T>
function does the necessary type casting for us. When we pass the action map in action_types.ts
to createActionConstantsMap
, we need to pass our desired payload structure as the generic argument to actionType<T>
. Here's what actionType
is doing in the background.
// reduxTypes.ts
export function actionType<TPayload>() {
return null as ActionType<TPayload>;
}
This function will cast the value of our action map at that specific key (in our case SEND_FLASH_MESSAGE
) as an ActionType<TPayload>
type with the given payload interface regardless of the value. The function will return null
, but a properly cast null
value for each action key.
The createActionConstantsMap
function will take care of creating an actual string value constant to be passed around and referenced by the reducers. As it is, the object we pass into createActionConstantsMap
in action_types.ts
, has a key value SEND_FLASH_MESSAGE
and a value of null
typed as an ActionType<FlashMessage>
. Null isn't particularly useful for distinguishing actions inside the reducers, so we'll generate a name-spaced string for each key within the createActionConstantsMap
function.
// actionUtils.ts
export function createActionConstantsMap<T extends ActionTypeMap>(prefix: string, input: T): T {
prefix = prefix.toUpperCase() + '_';
const map = Object.keys(input).reduce((acc, key) => {
const namespacedActionType = prefix + key;
return Object.assign(acc, { [key]: namespacedActionType });
}, {}) as T;
return map;
}
The typing comes in through a combination of type inference and type casting inside this function. When we pass our input
argument, it has a structure as follows:
{
SEND_FLASH_MESSAGE: null, // Cast as ActionType<FlashMessage>
}
This type is maintained inside createActionConstantsMap
as the generic type T
which is inferred by Typescript. Next, we generate an new object with the same key values, but name-spaced strings, and then cast that object as type T
. This is the key point from whence the typing magic flows. When we reference the SEND_FLASH_MESSAGE
property on the action types map, it has the proper payload interface bound to it.
Reducers
We'll write out a single reducer that will manipulate the application state based on our SEND_FLASH_MESSAGE
action.
We placed all of our reducer logic inside a single reducers
directory split up by roughly by component. Our directory structure will look as follows:
reducers
└── flash_message
├── flash_message_reducer.ts
├── flash_message_selectors.ts
├── flash_message_state.interface.ts
└── index.ts
I'll walk through the structure and responsibility of each file in turn.
Reducer Interface
We'll first define the structure of the subsection of the root state the flash message reducer deals with.
// flash_message_state.interface.ts
import { FlashMessage } from '../../interfaces/flash_message.interface';
export interface FlashMessageState {
flashMessage: FlashMessage;
}
These subsections of the root state will be defined for each reducer section and collated into a single RootState
interface.
Next, we'll create our reducer making use of some helper functions to maintain strong typing.
Reducer Logic
// flash_message_reducer.ts
import { FlashMessageState } from './flash_message_state.interface';
import flashMessageActionTypes from '../../actions/user_buttons/action_types';
import { createActionHandler, createReducer } from '../../utils/redux';
const handleAction = createActionHandler<FlashMessageState>();
const handleFlashMessageSent = handleAction(flashMessageActionTypes.SEND_FLASH_MESSAGE, (state, action) => {
const { message, color } = action.payload;
return Object.assign({}, state, {
flashMessage: {
message,
color
}
});
});
const initialState: FlashMessageState = {flashMessage: {message: '', color: ''}};
export const flashMessageReducer = createReducer(initialState, [
handleFlashMessageSent
]);
The first thing we need to do is create a function handleAction
through the createActionHandler
helper function. createActionHandler
will bind a generic type as the state that the reducer interacts with, in this case FlashMessageState
.
// actionUtils.ts
...
export function createActionHandler<TState>() {
return <TPayload>(actionType: ActionType<TPayload>, handler: ActionHandler<TState, TPayload>): TypedActionHandler<TState, TPayload> => {
return Object.assign(handler, { actionType: actionType.toString() });
};
}
The handleAction
function that is returned, takes the action type we're listening for, imported from our map created earlier, and the reducer handler function which contains the reducer logic. handleAction
will return a new object with a handler
property corresponding to the handler function and an actionType
property which is the string constant to trigger that reducer logic.
This function is essential for maintaining the proper typing for the both the state and the action payload. The handler function is now properly bound to the state type passed in through the createActionHandler
function, and the payload type it can expect through the action type argument. In our case, <TState>
is bound to FlashMessageState
type after invoking createActionHandler
, and <TPayload>
is inferred as <FlashMessage>
after passing in flashMessageActionTypes.SEND_FLASH_MESSAGE
.
Once we have our reducer function, handleFlashMessageSent
, we will pass it to our createReducer
utility function.
In addition to handling initial state, createReducer
will create a closure around an object variable mapping each of the passed handler's actionType
property as a key and each handler
function as the value.
// reducerUtils.ts
import { Action, Reducer, TypedActionHandler, TypedActionHandlerMap } from './';
interface GetInitialState<T> {
(): T;
}
export function createReducer<TState>(initialState: TState|GetInitialState<TState>, handlers: TypedActionHandler<TState, any>[]): Reducer<TState> {
const handlerMap = handlers.reduce((acc, handler) => {
return Object.assign(acc, { [handler.actionType]: handler });
}, {} as TypedActionHandlerMap<TState>);
const getInitialState = typeof initialState === 'function' ? initialState : () => initialState;
return (state: TState, action: Action<any>) => {
if (typeof state === 'undefined') {
state = getInitialState();
}
const handler = handlerMap[action.type];
return handler ? handler(state, action) : state;
};
}
The function createReducer
returns, then references the passed in action type and invokes the given handler passing in the current state. All handlers will be composed using this function as a single reducer, and they will in turn be exported and collated into the root reducer.
Reducer Selectors
For each reducer, we'll create a set of selectors which we can use to get access to specific data on the state.
// flash_message_selectors.ts
import { createStateSelector } from '../../utils/redux';
export const getFlashMessage = createStateSelector(state => state.flashMessage);
We'll import the createStateSelector
helper function which will use the reselect
library and add some typings.
// selectorUtils.ts
import { createSelector } from 'reselect';
import { SelectorWithoutProps } from './reduxTypes';
export function createStateSelector<TResults>(selector: SelectorWithoutProps<TResults>): SelectorWithoutProps<TResults> {
return selector;
}
CreateStore
Once we have all our reducers set up, we'll import them into a createStore
function which will combine them together along with our root state and any middleware, and create a store which we can pass to our Provider.
// createStore.ts
import * as Redux from 'redux';
import thunk from 'redux-thunk';
import { Dispatch, RootState } from '../utils/redux';
import { flashMessageReducer, stateKey as flashMessageStateKey } from '../reducers/flash_message';
const rootReducer = Redux.combineReducers<RootState>({
[flashMessageStateKey]: flashMessageReducer
});
export interface Store extends Redux.Store<RootState>{
dispatch: Dispatch;
};
export default (initialState = {}) => {
const middleWare = Redux.applyMiddleware(thunk);
return Redux.createStore<RootState>(rootReducer, initialState as RootState, middleWare);
};
createStore
will bring in all reducers, in this case just the flashMessageReducer
, and map them by a stateKey
value which is exported from each reducer's index.ts
file. createStore
will also reference the RootState
interface where we'll define the structure of our application state. Each key in the Root State object should map to the stateKey
value for each reducer. In our example, the state key for the flash message reducer is the string 'flashMessage'
the same key on Root State that this reducer is responsible for. As more reducers and pieces of state are added, the Root State will expand. This will give us a single source of truth to reference the structure of our application state.
// rootState.ts
import { FlashMessageState } from '../../reducers/flash_message';
export interface RootState {
flashMessage: FlashMessageState;
};
Wiring Up Components
Linking typed state and action creators to the components works very much the same way as it does in plain old javascript Redux. Of our two components flash_message.tsx
will be concerned with displaying a state property, and user_buttons.tsx
will dispatch our action.
The third party typings files available for React/Redux do most of the Typescript work for us. We need to pass component property interfaces into two places. The first place, when we declare the component, we'll need to pass in an intersection type of a composition of the component's own props, the Redux state props it listens to, and the Redux dispatch props. The second place we need to pass interfaces in is the connect
Redux function. connect
takes in three generic type arguments, connect<TStateProps, TDispatchProps, TOwnProps>
.
Out of the box, we would need to define all these interfaces to mirror the types returned from our mapStateToProps
and mapDispatchToProps
functions. We can save ourselves some work by using some Typescript type inferencing magic. The returnType
helper function, when given a function as an argument, will return a null
value given the same type of the passed function's return type.
// typeUtils.ts
interface Func<T> {
([...args]: any): T;
}
export function returnType<T>(func: Func<T>) {
return null as T;
}
We can then use Typescripts obscurely named typeof
operator to access the type of the null
value. In our example, we'll pass in the mapStateToProps
and mapDispatchToProps
functions to returnType
which will automatically infer the type of the Redux state our component is listening to.
In practice, this is what our flash_message
component will look like.
// flash_message.tsx
import * as React from 'react';
import { connect } from 'react-redux';
import { getFlashMessage } from '../../reducers/flash_message';
import { bindActionCreators, RootState, Dispatch, returnType } from '../../utils/redux';
function mapStateToProps(state: RootState) {
return {
flashMessage: getFlashMessage(state)
};
}
interface FlashMessageOwnProps {
}
const stateGeneric = returnType(mapStateToProps);
type FlashMessageStateProps = typeof stateGeneric;
type FlashMessageProps = FlashMessageStateProps & FlashMessageOwnProps;
class FlashMessage extends React.Component<FlashMessageProps, {}> {
render() {
const { color, message} = this.props.flashMessage.flashMessage;
return (
<p style={{color}}>{message}</p>
);
}
}
export default connect<FlashMessageStateProps, {}, FlashMessageOwnProps>(mapStateToProps)(FlashMessage);
For the mapStateToProps
function, we'll use a selector function we defined in our reducer directory.
By using returnType
and Typescript's typeof
, we can inference the return type of mapStateToProps
and can appropriately type our component's props. The user_buttons
component works much the same way except we're concerned with the mapDispatchToProps
function instead.
At this point, we have all the typed Redux wired up for our trivial little application. While Typescript has some initial overhead to get over, we've found it a huge help once our components get more complex. It gives us a compile time guard against passing in woefully inappropriate data into our action creators and allows us to know exactly what our reducers are going to be dealing with. With a few essential patterns and utility functions, we can greatly reduce the amount of duplication necessary to keep those actions reducing smoothly.