Carl Rippon

Building SPAs

Carl Rippon
BlogBooks / CoursesAbout
This site uses cookies. Click here to find out more

Strongly-typed React Redux Code with TypeScript

February 05, 2019
reacttypescript

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 …

React, Redux and TypeScript

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
  • GotPeople. 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
  • 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<
  // The type of the last action to be dispatched - will always be promise<T> for async actions
  Promise<IGotPeopleAction>,
  // The type for the data within the last action
  IPerson[],
  // The type of the parameter for the nested function
  null,
  // The type of the last action to be dispatched
  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<
  // The type of the last action to be dispatched - will always be promise<T> for async actions
  Promise<IPostedPersonAction>,
  // The type for the data within the last action
  IPostPersonResult,
  // The type of the parameter for the nested function
  IPostPerson,
  // The type of the last action to be dispatched
  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); // when a new action is created, this helps us remember to handle it in the reducer
  }
  return state;
};

// tslint:disable-next-line:no-empty
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.

TypeScript type narrowing

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

We can make our redux code strongly-typed in a fairly straightforward manner. The way that TypeScript narrows the action type in reducers is really smart and the use of never is a nice touch. Typing asynchronous action creators is a bit of a challenge and there may well be a better approach but it does the job pretty well.

If you to learn more about using TypeScript with React, you may find my course useful:

Using TypeScript with React

Using TypeScript with React
Find out more

Want more content like this?

Subscribe to receive notifications on new blog posts and courses

Required
© Carl Rippon
Privacy Policy