A New Redux Action Pattern for TypeScript 2.4+

In September 2016, I wrote a post about a pattern for strongly typed Redux reducers in TypeScript. Since then, the TypeScript team has been busy enhancing the language, and recent changes in TypeScript have enabled a much more natural, boilerplate-free pattern for strongly typed actions and reducers in TypeScript.

Our motivating example is to be able to write reducers like this:

Code example of a reducer that is fully typed by TypeScript.

This reducer is for a simple counter, which can increment or decrement a value in response to dispatched actions. We can dispatch(incrementCounter(5)) to increment the counter by five, or dispatch(decrementCounter(2)) to decrement by two, for example.

In particular, we want to maximize our leverage of TypeScript with minimal boilerplate. TypeScript should tell us if we use an action key that’s unsupported, and it should know that, when we switch on action.type, we’re narrowing the type of the action down to a specific possibility. Note that the action is inferred to be an IncrementAction in the screenshot above within the scope of the INC case.

Specifically, we want static type-checker support to:

  • Ensure that we switch on valid action types
  • Understand the properties of types in specific cases, allowing us to auto-complete property names such as by, as well as know those properties’ types
  • Warn us if we fail to add a default case, which will cause Redux to break when actions are dispatched by third-party libraries

Let’s look at how to do this in TypeScript 2.4 and up.

1. Define Action Type Keys

Our first step is to define an enumeration of all of the action type keys we wish to dispatch on in our application:

  export enum TypeKeys {
    INC = "INC",
    DEC = "DEC",
    OTHER_ACTION = "__any_other_action_type__"
  }

TypeScript 2.4 added support for string enums, which solve a number of problems with prior approaches. String enums give us a convenient way to define string constants that are narrowly typed to specific values – important for the rest of the technique. (An alternative is literal types – see my last post on the topic.)

(We’ll explain OTHER_ACTION shortly.)

2. Create Action Types

Next, we define types for our specific actions. Define each action type as follows:

  export interface IncrementAction {
    type: TypeKeys.INC;
    by: number;
  }

The important point here is the definition of type. Note that we’re defining type to have the type TypeKeys.INC. This tells TypeScript that all IncrementAction have exactly one value as their type, enabling TypeScript to prove that if we test action.type == TypeKeys.INC, we will have a by property that is a number.

How additional properties are organized is irrelevant – you can use flux standard actions with this technique as well.

3. Create a Type Representing All App Actions

Next, define a type representing all actions in your application by simply or-ing together all of your action types:

  export type ActionTypes = 
    | IncrementAction 
    | DecrementAction 
    | OtherAction;

We tend to do this at the top of our actions file. TypeScript types can refer to types defined later in the file without issue.

4. Create an “Other” Action Type

The list of actions we handle in our reducers is almost never the complete list of actions flowing through Redux. Third-party plugins and Redux built-in actions happen as well, and our reducers need to handle them appropriately. It’d be nice to get help from TypeScript so that we don’t forget.

Our preferred approach for this is to define an OtherAction type (which we never dispatch) that lives in our ActionTypes, so TypeScript will warn us if it’s not handled.

  export interface OtherAction {
    type: TypeKeys.OTHER_ACTION;
  }

We’ve stated here that OtherAction has a type with the value TypeKeys.OTHER_ACTION. This is a useful fiction. No action with type ”__any_other_action_type__" is ever dispatched.

Instead, we always make TypeScript happy in our reducers by including a default case. TypeScript thinks it’s there to handle OtherAction, but it’s really there for everything else.

Interlude: An Example reducer

With this foundation we can define reducers in terms of our actions, such as the following for a simple counter that increments/decrement a value:

function counterReducer(s: State, action: ActionsTypes) {
  switch (action.type) {
    case Actions.TypeKeys.INC:
      return { counter: s.counter + action.by };
    case Actions.TypeKeys.DEC:
      return { counter: s.counter - action.by };
    default:
      return s;
  }
}

TypeScript understands our types in a deep way. It’s able to prove that, within the INC case, our action is an IncrementAction and therefore has a by property. That property is not availability in our default branch, as it’s not common to other actions.

Additionally, our default case is enforced by TypeScript because of our OtherAction type. Without that, TypeScript would think our case statement exhaustive, when it is not.

5. Action Builders

Action builders are also simple – just regular functions. We do recommend explicitly typing the return value so that TypeScript will check the action builder for compliance with the type:

const incrementCounter = (by: number): IncrementAction => ({
  type: TypeKeys.INC,
  by
});

And that’s it! Fully typed actions and reducers for Redux with minimal boilerplate.

Conversation
  • sudo says:

    Enums are only allowed to define a set of named numeric constants

  • Tom says:

    Thanks, I really like the pattern.

    However it seems the builder is really not that useful considering the added complexity.

    const incrementCounter = actionBuilder(TypeKeys.INC)(“by”);

    vs

    const incrementCounter = (by: number): IncrementAction => ({ type: TypeKeys.INC, by });

    • Drew Colthorp Drew Colthorp says:

      Thanks, Tom.

      Yeah, perhaps you’re right about the actionBuilder, esp. given that way of writing the builder functions. I’d been using classic JS functions for builders out of habit, but switching them out for arrow functions helps with the noise a lot. Thanks for the feedback.

      • Charlie says:

        you should consider updating your article to reflect this. I’d like the share it as a reference to my team but the added complexity from the builder functions is a bit off-putting. (arrow: function)=> {return ‘FTW’}

        • Philip says:

          Agreed. Was oohing and aahing at everything until I reached the section on Action Builders. Maybe even go so far as to remove Action Builders from this article entirely (you could always create a separate article that simply explores it as a concept?). Whilst I’d hate to see codebases using that curried ActionBuilder pattern, the research you’ve done on string enums for action types is excellent — I hope to see it widely adopted.

        • Drew Colthorp Drew Colthorp says:

          Done. :-)

  • Geir Sagberg says:

    Very good post, had a well-received presentation on a local Meetup using your typed actions along with typed forms from https://medium.com/@steida/the-boring-react-redux-forms-a15ee8a6b52b, for a really nice type safe form development experience.

    Also agree that the actionBuilder is a bit too magic, thanks for updating the blog post with regular arrow function action creators.

  • Pierre says:

    Hi Drew,
    Thanks for the tips, really nice way of putting it :)
    Just out of curiosity can your ActionBuilder be seen somewhere?
    Cheers

  • Benny Powers says:

    Howdy, I tried this out but got these maddening errors:

    “`ts
    export enum TypeKeys {
    SET_LOADING = ‘SET_LOADING’,
    }

    function setLoading(loading: boolean): Actions.SetLoadingAction {
    const type: Actions.TypeKeys.SET_LOADING = ‘SET_LOADING’;
    return {type, loading}
    }
    “`

    • Benny Powers says:

      Type ‘{ type: “SET_LOADING”; loading: boolean; }’ is not assignable to type ‘SetLoadingAction’.
      Types of property ‘type’ are incompatible.
      Type ‘”SET_LOADING”‘ is not assignable to type ‘TypeKeys.SET_LOADING’.’

      • Benny Powers says:

        I can see I missed the point of string enums. Instead of sending the string literal value in the action, I instead lookup the value from the enum: {type: Actions.TypeKeys.SOME_ACTION}.

        Hope that helps someone visiting from Google.

        • Drew Colthorp Drew Colthorp says:

          Glad you sorted it out, Benny! My original approach to actions/reducers in typescript (linked to at the start of the article) used literal string types, but you tend to run into some undesirable behavior if you go that route

          (Typescript’s freshness behavior, which can be surprising to typescript newcomers and require redundant type declarations).

          • Robert Manzano says:

            I’m sort of having the same issue and I’m confused… In:

            Actions.TypeKeys.INC

            Where does Actions come from?

          • Drew Colthorp Drew Colthorp says:

            Hi Robert,

            The Actions in this case would be from something like import * as Actions from 'client/actions'.

  • Christian says:

    One simple suggestion is to create a generic interface for action type:
    interface BaseAction {
    type: T,
    by: U
    };
    Then IncrementAction becomes:
    interface IncrementAction extends BaseAction {}
    and DecrementAction becomes:
    interface DecrementAction extends BaseAction {}

    • Christian says:

      Silly editor stripped the generics thinking they were tags…but you get the picture.

      • Drew Colthorp Drew Colthorp says:

        Something like this would be a useful pattern if you’re using FSA or something, for sure.

        In this particular example, hese actions happened to both have a `by` property by coincidence – it’s not something that I was thinking would generalize to all actions.

  • Mark says:

    Nice article! Just a minor thing, but defining the TypeKeys is not necessary for this technique to work, tsc will notify you if you want to use a string which is not used as a type. It is still nice though to store your strings in constants.

    • Drew Colthorp Drew Colthorp says:

      Thanks, Mark! The string enum tends to work better than literal string types due to TypeScript’s freshness behavior. It’s very easy to end up in situations where TS widens a literal to all of `string` when you really want to preserve the fact that it was some specific string. String enums, in addition to giving you constants, help avoid some of these situations and make things a little easier to deal with.

      More about freshness: https://basarat.gitbooks.io/typescript/content/docs/types/freshness.html

      • Mark says:

        To be clear, I agree with it being a best practice, I was just trying to point out that saying ‘important for the rest of the technique’ is not true :-)

        I’m aware of the concept of freshness in TypeScript, but can’t really think of how it can cause you problems in this scenario.

  • Jean-Paul Finné says:

    Hi Drew,
    I think it is even simpler if you use classes instead of interface for Actions.

    You don’t need the incrementCounter function if you define the actions as this:
    export class IncrementAction {
    type = TypeKeys.INC;
    constructor(public by: number) {}
    }

    then create the action by using new: new IncrementAction(1);

    • Jean-Paul Finné says:

      Oups, sorry, we need to keep type safety:

      export class IncrementAction {
      type: TypeKeys.INC = TypeKeys.INC;
      constructor(public by: number) {}
      }

      • Drew Colthorp Drew Colthorp says:

        That would certainly work. However, there is downside to this approach.

        With classes, you need to use the `new` keyword everywhere you’re dispatching one of these actions. This (a) clutters up the component dispatching the action and (b) pushes implementation details of your actions down into your component layer, because you’ve now coupled your action instantiation to the `class` language mechanism. You’d be able to tell which actions are data and which are e.g. thunks just by looking at the dispatching code. I prefer to keep my front-end more decoupled from these decisions.

        (You could also wrap the class instantiation in an action builder, but then you’re really not gaining much through the use of `class` to begin with.)

        On a separate-but-related note, we’re also mostly avoiding the use of `class` in general unless we’re explicitly programming in an OO style. We’re doing this sparingly, however, so not really using `class` much in practice. YMMV.

  • Dean Moses says:

    Drew, thanks for the post. How would you incorporate asynchronous thunk actions into this model? Seems like the actions would need to be a const or a class in order to have functions that dispatch events.

    • Drew Colthorp Drew Colthorp says:

      Thunks just work as normal – some of our action builders return thunks instead of action objects, and we just use our other action builders with the `dispatch` passed to the thunk. There really is no impact, as the differences from untyped redux are limited to the implementations of reducers and action builders.

      Redux saga is similar – no impact, but with sagas we use our `TypeKeys` enum with `takeEvery` etc. (Sagas adds some additional wrinkles because `yield` syntax is always dynamically typed in TypeScript, but that’s really a more general issue independent of action typing.)

  • velohomme says:

    Very helpful. Thank you.

    Suggestion: using singular names for enums, not plural. Enum values are enumerating the candidates for a category. SEX.male makes more sense than SEXES.male.

  • Ats says:

    Drew, thanks for sharing the post!
    I think I’ve found a way to solve this problem with less boilerplate code and more type-safety while even allowing more flexibility (different types of action payloads for related action reducer functions).
    I just open-sourced my solution here: https://github.com/atsu85/redux-actions-ts-reducer#more-information – You’ll find examples, instructions and tests from there. Let me know what You think!

  • Comments are closed.