In the following post, I’ll introduce Redux Guards, a pattern for Redux and TypeScript. It is a convenient and powerful way to type your actions, as it leverages implicit returned types from your action creators, reducing unnecessary boilerplate without sacrificing type safety. This pattern also works well with middleware, enabling transformation of your dispatched actions into different shapes with similar type structures (i.e. redux-pack).

The pattern, which I’m calling Redux Guards, is a three-part solution to typing Redux, corresponding to the three aspects of Redux itself:

Typed action creators, with inferred typing

Typed return values from dispatch , accounting for any middleware

, accounting for any middleware Type guards for actions within our reducer

If you would like to skip the details and jump straight into the code, head on over to GitHub: redux-guards and check out the examples. If you like what you see, give it a star! It would make my day! 😄

What follows is a detailed dive into the problems of typing Redux, some existing solutions, and details into the Redux Guards approach. If you have any questions, please open an issue on GitHub.

The Problem

Due to the fundamental design of Redux, we lose a great deal of compile-time information across the boundary of dispatch() and the reducer. Consider the following basic action creators:

While they conform the Redux FSAs, these two simple action objects have completely different payload data. Within our reducer, when our code narrows execution to a single action, it should know the action’s type. However, even with the simplest possible reducer, we have a problem with the default Action type:

At compile time, the type Action only declares that it must have a key of type . We are left to figure out and explicitly assert the action type at runtime. In order to work with with our actions, we must cast then as a specific type or as the unsafe any type. Let’s take a look at some solutions that help TypeScript know more about our actions at compile-time.

Explicit Types

Explicit types are the simplest, most straightforward method to adding type information:

While being simple on the surface, this pattern adds a reasonable burden on the developer:

Problem: Redundant type declarations

Every action must be explicitly typed, causing developers to spend more time matching return values of action creators to the type definition.

When action shapes change, developers must once again re-type the return value

Maintaining type definitions is sometimes a necessary cost in writing safe code. However, in nearly all Redux codebases there is only a single action creator for any action. TypeScript should be able to infer the returned type for us, and that inferred type would be the source of truth across our application.

Problem: Casting in the reducer

When we execute a branch in our reducer based on the action.type , we know that our Action is of a certain type, since actions correspond directly to action.type . Developers being required to typecast for every action doesn’t scale well, and is haphazard.

When developers get sloppy, static typing may get thrown to the wayside in lieu of the any type 💀. Through a good abstraction, we can make our own lives easier, which will make us happier and result in better code!

Discriminated Unions

One approach for avoiding casting within the reducer is leveraging Discriminated Unions. Instead of declaring your reducer’s action as an Action type, you declare that it is a union type of every action your application:

This pattern is also called ADTs or Tagged Unions. In short, TypeScript is sophisticated enough to know that, of all possible actions, only a subset will match action.type === ACTION_NAME , and TypeScript correctly narrows the type appropriately. The result is type-safe code in the associated scopes.

We still have a problem with our codebase:

We still must explicitly type our actions

We now must also create a massive union type for all of our actions

These issues still do not scale very well, making for a somewhat annoying developer experience. Furthermore, when actions within the reducer do not match up with actions dispatched (in the case of middleware), this pattern becomes unwieldy.

Implicit Types

As much as possible, we want the compiler to do work for us. In TypeScript, when we declare an object of a certain shape, its type is automatically inferred. For example:

With action creators, we use a simple function that returns a plain object. This complicates things for static typing, since we don’t have a straightforward way to “reach in” to the function and get its return type implicitly. There is an upcoming feature that will allow for this, but we will see shortly that another method of obtaining type information has useful features especially well-suited for Redux.

Let’s focus on one of our action creators:

TypeScript infers the type correctly:

When we call addToCart(['foo']) , the return value will have correct type information associated with it. We want that same information when we narrow the type within our reducer. We explored discriminated unions above, but let’s look at another tool TypeScript provides us.

Type Guards

Type Guards are a feature that allow us to narrow the type of a variable when a condition is true. With type guards, we define a function that checks if our actions are of a certain type, and safely cast them to the correct type. In the following code, isAction() is a hypothetical type guard that would accomplish what we’re looking for:

Let’s try to implement that type guard:

In the guard expression, action is ??? , the placeholder ??? is where our casted type would be. We need to provide the actual type there, but we lack information on which type that is. Although we have the actionType string, without any context, it doesn’t point us to the actual type of the action. We need a way to connect the dots.

Since we lack an explicitly defined type definition, the source of truth for our action’s type is the action creator itself. Instead of passing in the actionType string, let’s use the action creator function to match against the current action:

Now, leveraging generics, if we pass in the action creator function itself, we are able to “extract” the type information of the returned action. With this approach, we have a tradeoff, and come across another issue: we don’t have the action.type string to compare against! Since the guard evaluation code is at runtime, and TypeScript doesn’t generate any runtime information, we can’t get the actionCreator 's returned type key without actually calling function. And since the function is generic, we cannot reliably call it.

A simple solution would be to accept a third parameter, e.g. isAction(action, actionCreator, actionType) , but that is verbose and redundant since action creators and their action.type are 1:1.

The alternative solution is to add some runtime information to the action creator via a helper method. We have to adjust how we create our actions slightly:

Our makeAction helper is a second-order function. The first argument accepts a type string and associates that with the action creator function. The second call to the helper is where we define our action creator, accepting any parameters needed.

The returned value from our helper method is the action creator, but with extra runtime information tacked on to the prototype of the function. This will enable us to use type guards!

Now, within our reducers, we can do this:

And, since that function has extra information on it, we can write our type guard:

In the examples above, both isAction and the makeAction are helpers that are rarely modified once inside your codebase. In practice, they are “set it and forget it”, and have served as very obvious and useful abstractions. Here is an example set of actions and corresponding usage in reducers:

As you can see, once the pattern has been established, it is very simple to follow and has clear intentions.

Dispatch Return Values

Using makeAction does introduce one issue in our codebase: The standard typing for dispatch isn’t compatible with our action types, since the returned value of our action creator lacks the type key. We omit this redundant type key from our actions, since it is present and self-evident when we call makeAction . To get around this, we will extend the type of dispatch :

Now, any action that has a payload key may be dispatched, even if its type lacks a type key. Note, that even though we allow this in our typings, it is not valid for actions to omit a type value at runtime. This is just a convenience for our typings—the actual action object still has a type key in it. Within makeAction , our code obscures its type by casting it to any . This removes the need for developers to redundantly use a type constant twice:

The latter is simpler, and warrants extending dispatch for our codebase.

Improving makeAction and isAction

Here is the fully typed and robust version of makeAction :

I won’t go over this in detail, but this provides our exported action creators with all the information to work with TypeScript as you would expect.

Typed Middleware

In most Redux projects, middleware is necessary to work asynchronously and reduce boilerplate code. For a simple middleware such as redux-thunk , which doesn’t actually dispatch actions itself, we do not need to make any affordances. However, there are other useful middleware that dispatch actions based on the originally-dispatched action. One that I’ve enjoyed across many projects is redux-pack.

redux-pack , along with many other middleware, take an action of a certain shape and dispatch a different action with a subset of your previous action’s shape. For example, with redux-pack , if we dispatch an action { promise: Promise<T> } , redux-pack will in turn dispatch an associated action: PackAction<MyState, T, {}, {}, {}> . It takes our Promise, when resolved, and gives us an object that contains success/failure information. Many middleware operate in the same manner, reshaping our actions to be more easily consumed in our reducer.

We can extend the Redux Guards pattern to account for any middleware. For any given middleware, there are three behaviors for which we need to account:

When we define our action creator, we want to ensure it conforms to the correct shape.

When we dispatch our middleware action, the return value of dispatch may be different than the original action.

For our type guard, the original action needs to be transformed into the shape our reducer receives.

Ensuring correctly-defined actions

In order for middleware to trigger, a dispatched action must conform to a certain shape. For redux-pack , actions must be of type Action & { promise: Promise<T> } . We wouldn’t want developers creating actions with a typo, such as { promisse: Promise } without a helpful error. To facilitate this, we define action creator helpers for each middleware:

When we call our helper makePackAction , it ensures that we only provide action creators that conform to the middleware shape. Any pack-action that lacks a promise key will generate a compiler error:

This pattern scales linearly with your middleware. In most Redux projects, the amount of transforming middleware is usually kept to a minimum, and in practice this has been very maintainable.

Matching actions to dispatch return value

Our middleware will cause dispatch to deviate from its standard behavior. In this example, dispatch will now return a promise. We can further extend Dispatch typings to compensate:

We can now work with dispatch return values correctly:

Additional Type Guard

The last piece to our pattern is a type guard for our promise middleware. Here’s the code:

The type guards all follow the same basic pattern. They take a shape in, and convert it to an output shape. In this example, it transforms a generic type { meta?: U, promise: Promise<T> } and transforms it to Action & { meta?: U, payload: PackAction<T> } . This pattern can be used similarly in any middleware situation.

Wrapping Up

My small team at Instrument has been using this pattern for a few months now, and most of the kinks have been ironed out. Once the pattern is in play, it adds a great deal of safety to your code, and appropriately throws compiler errors when your reducer and action shapes do not match up. It has proven to be a really enjoyable abstraction.

I hope you find this pattern useful! If you have any questions or feedback, feel free to open an issue on the repository.