Redux is a popular library used to manage state in React apps. How can we make our Redux code strongly-typed with TypeScript - particularly when we have asynchronous code in the mix? Let’s find out by going through an example of a store that manages a list of people …

UPDATE: A more up-to-date post on React, Redux and TypeScript can be found here.

State

Let’s start with the stores state object:

interface IPeopleState { readonly people : IPerson [ ] ; readonly loading : boolean ; readonly posting : boolean ; } export interface IAppState { readonly peopleState : IPeopleState ; } const initialPeopleState : IPeopleState = { people : [ ] , loading : false , posting : false , } ;

So, our app just contains an array of people. We have flags in the state to indicate when people are being loaded from the server and when a new person is being posted to the server. We’ve declared the state as readonly so that we don’t accidentally directly mutate the state in our code.

Actions

A change to state is initiated by an action. We have 4 actions in our example:

GettingPeople . This is triggered when a request is made to get the array of people from the server

. This is triggered when a request is made to get the array of people from the server GotPeople . This is triggered when the response has been received with the array of people from the server

. This is triggered when the response has been received with the array of people from the server PostingPerson . This is triggered when a request is made to the server to add a new person

. This is triggered when a request is made to the server to add a new person PostedPerson. This is triggered when the response has been received from the server for the new person

Here’s the code:

export interface IGettingPeopleAction extends Action < "GettingPeople" > { } export interface IGotPeopleAction extends Action < "GotPeople" > { people : IPerson [ ] ; } export interface IPostingPersonAction extends Action < "PostingPerson" > { type : "PostingPerson" ; } export interface IPostedPersonAction extends Action < "PostedPerson" > { result : IPostPersonResult ; } export type PeopleActions = | IGettingPeopleAction | IGotPeopleAction | IPostingPersonAction | IPostedPersonAction ;

So, our action types extend the generic Action type which is in the core Redux library, passing in the string literal that the type property should have. This ensures we set the type property correctly when consuming the actions in our code.

Notice the PeopleActions union type that references all 4 actions. We’ll later use this in the reducer to ensure we are interacting with the correct actions.

Action creators

Actions creators do what they say on the tin and we have 2 in our example. Our action creator is asynchronous in our example, so, we are using Redux Thunk. This is where the typing gets a little tricky …

The first action creator gets the people array from the server asynchronously dispatching 2 actions along the way:

export const getPeopleActionCreator : ActionCreator < ThunkAction < Promise < IGotPeopleAction > , IPerson [ ] , null , IGotPeopleAction >> = ( ) => { return async ( dispatch : Dispatch ) => { const gettingPeopleAction : IGettingPeopleAction = { type : "GettingPeople" , } ; dispatch ( gettingPeopleAction ) ; const people = await getPeopleFromApi ( ) ; const gotPeopleAction : IGotPeopleAction = { people , type : "GotPeople" , } ; return dispatch ( gotPeopleAction ) ; } ; } ;

ActionCreator is a generic type from the core Redux library that takes in the type to be returned from the action creator. Our action creator returns a function that will eventually return IGotPeopleAction . We use the generic ThunkAction from the Redux Thunk library for the type of the nested asynchronous function which has 4 parameters that have commented explanations.

The second action creator is similar but this time the asynchronous function that calls the server has a parameter:

export const postPersonActionCreator : ActionCreator < ThunkAction < Promise < IPostedPersonAction > , IPostPersonResult , IPostPerson , IPostedPersonAction >> = ( person : IPostPerson ) => { return async ( dispatch : Dispatch ) => { const postingPersonAction : IPostingPersonAction = { type : "PostingPerson" , } ; dispatch ( postingPersonAction ) ; const result = await postPersonFromApi ( person ) ; const postPersonAction : IPostedPersonAction = { type : "PostedPerson" , result , } ; return dispatch ( postPersonAction ) ; } ; } ;

So, the typing is fairly tricky and there may well be an easier way!

Reducers

The typing for the reducer is a little more straightforward but has some interesting bits:

const peopleReducer : Reducer < IPeopleState , PeopleActions > = ( state = initialPeopleState , action ) => { switch ( action . type ) { case "GettingPeople" : { return { ... state , loading : true , } ; } case "GotPeople" : { return { ... state , people : action . people , loading : false , } ; } case "PostingPerson" : { return { ... state , posting : true , } ; } case "PostedPerson" : { return { ... state , posting : false , people : state . people . concat ( action . result . person ) , } ; } default : neverReached ( action ) ; } return state ; } ; const neverReached = ( never : never ) => { } ; const rootReducer = combineReducers < IAppState > ( { peopleState : peopleReducer , } ) ;

We use the generic Reducer type from the core Redux library passing in our state type along with the PeopleActions union type.

The switch statement on the action type property is strongly-typed, so, if we mistype a value, a compilation error will be raised. The action argument within the branches within the switch statement has its type narrowed to the specific action that is relevant to the branch.

Notice that we use the never type in the default switch branch to signal to the TypeScript compiler that it shouldn’t be possible to reach this branch. This is useful as our app grows and need to implement new actions because it will remind us to handle the new action in the reducer.

Store

Typing the store is straightforward. We use the generic Store type from the core Redux library passing in type of our app state which is IAppState in our example:

export function configureStore ( ) : Store < IAppState > { const store = createStore ( rootReducer , undefined , applyMiddleware ( thunk ) ) ; return store ; }

Connecting components

Moving on to connecting components now. Our example component is a function-based and uses the super cool useEffect hook to load the people array when the component has mounted:

interface IProps { getPeople : ( ) => Promise < IGotPeopleAction > ; people: IPerson[]; peopleLoading: boolean; postPerson: ( person: IPostPerson ) => Promise < IPostedPersonAction > ; personPosting: boolean; } const App: FC < IProps > = ({ getPeople, people, peopleLoading, postPerson, personPosting, }) => { useEffect ( ( ) => { getPeople ( ) ; } , [ ] ) ; const handleClick = ( ) => { postPerson ( { name : "Tom" , } ) ; } ; return ( < div > { peopleLoading && < div > Loading... </ div > } < ul > { people . map ( ( person ) => ( < li key = { person . id } > { person . name } </ li > ) ) } </ ul > { personPosting ? ( < div > Posting... </ div > ) : ( < button onClick = { handleClick } > Add </ button > ) } </ div > ) ; } ; const mapStateToProps = (store: IAppState) => { return { people : store . peopleState . people , peopleLoading : store . peopleState . loading , personPosting : store . peopleState . posting , } ; } ; const mapDispatchToProps = ( dispatch: ThunkDispatch<any, any, AnyAction> ) => { return { getPeople : ( ) => dispatch ( getPeopleActionCreator ( ) ) , postPerson : ( person : IPostPerson ) => dispatch ( postPersonActionCreator ( person ) ) , } ; } ; export default connect( mapStateToProps, mapDispatchToProps )(App);

The mapStateToProps function is straightforward and uses our IAppState type so that the references to the stores state is strongly-typed.

The mapDispatchToProps function is tricky and is more loosely-typed. The function takes in the dispatch function but in our example we are dispatching action creators that are asynchronous. So, we are using the generic ThunkDispatch type from the Redux Thunk core library which takes in 3 parameters for the asynchronous function result type, asynchronous function parameter type as well as the last action created type. However, we are using dispatch for 2 different action creators that have different types. This is why we pass the any type to ThunkDispatch and AnyAction for the action type.

Wrap up