Skip to content

Instantly share code, notes, and snippets.

@quicksnap
Created January 15, 2018 23:28
Show Gist options
  • Save quicksnap/82fe6f97ceacd9af641bef9e116f9a61 to your computer and use it in GitHub Desktop.
Save quicksnap/82fe6f97ceacd9af641bef9e116f9a61 to your computer and use it in GitHub Desktop.

Redux Guards for TypeScript

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 types returned from your action creators, which reduces unnecessary boilerplate without sacrificing type safety. This pattern also works well with middleware, allowing you to transform your dispatched actions into different shapes with similar type structures (redux-promise/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 dispatch return values, accounting for any middleware
  • Type guards for typed 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. If you like what you see, give it a star! It would make my day! 😄 If you have any questions, please open an issue.

What follows is a detailed dive into the problems of typing Redux, some existing solutions, and details into the Redux Guards approach.

The Problem

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

https://gist.github.com/4051b5f6a681d04b84d940fa3901ada2

At compile time, all Redux defines for any Action is that it must have a key of type. We are left to figure out and assert the action’s type at runtime. In order to work with them, we must cast then as a specific type, or as the unsafe any type. Let’s take a look at some solutions that let Typescript know more about our actions at compile-time.

Explicit Types

This is the simplest, most straightforward method to adding type information:

https://gist.github.com/7f02db51690c8e8fdaa388efac5412ac

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 type, we know that our Action is of a certain type, since actions should 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 and be replaced with 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 to avoid casting in the reducer is leveraging Discriminated Unions. Instead of declaring your reducer’s action as a plan Action type, you declare that it is a union type of every action your application:

https://gist.github.com/80f854d9b4fb22c1235162473e39cb81

This pattern is also called ADTs or Tagged Unions. TypesScript is sophisticated enough to know that, of all possible actions, only a subset will have action.type === '``ACTION_NAME``', and 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 reducer actions do not match up with actions dispatched because of middleware, this pattern begins to become 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:

https://gist.github.com/8f182c7443761e7ce6905e40679d9c60

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

Let’s focus on one of our action creators:

https://gist.github.com/408aeac55c6a93105ed906374b7a58f8

TypeScript infers the type correctly:

https://gist.github.com/1e39f66812d7e8d585c2c7d87d3ed1ab

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, isAction is a hypothetical type guard that would accomplish what we’re looking for:

https://gist.github.com/fa68d9a7c129d79fbc8cb28ba4f1899b

Let’s try to implement that type guard:

https://gist.github.com/fdb94fdb3c088bad437c1f5ea61614c7

In the guard expression, action is ??? , there’s a placeholder ??? where our casted type would be. We need to provide the actual type there, but 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:

https://gist.github.com/527561b2fe0caa7ca1f61ebf9caa38cd

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 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 slightly how we create our actions.

https://gist.github.com/be1e0d1e4d6ebbe59e41ee7f1b3ddb6d

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, optionally accepting any parameters needed for that action.

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

Now, within our reducers, we can do this:

https://gist.github.com/c3cd6b5c5e44918e80c57bb00e665e33

And, since that function has some runtime information tacked onto it, we can write our type guard!

https://gist.github.com/3395fef692fc49b9f38f78229a55070c

In the examples above, both isAction and the makeAction are helpers that are rarely modified once in your codebase. In practice, they have been “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:

https://gist.github.com/ee4237e2b92dc6492216f52f7c5bbe86

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:

https://gist.github.com/1615b60cf10d346a8d0a658cb935d32a

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, the code adds in the type value, but obscures its type by casting it to any. This removes the need for developers to redundantly use a type constant twice:

https://gist.github.com/032752dd1548268b124a2b9aeb587407

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:

https://gist.github.com/6dd75c763da669d1b833a58fd36fd196

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.

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, and redux-promise is very popular as well.

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 work, the action object 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 can create action creator helpers for every middleware for which we have corresponding actions:

https://gist.github.com/314e081cb72a14e65ff1e077618ec92a

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

https://gist.github.com/74ee9a7af4354b43b70d0bf6ecd308fa

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:

https://gist.github.com/ee27f5bb9c49e7f7dee16e0693071565

We can now work with dispatch return values correctly:

https://gist.github.com/518004fcd5090ac03c022fd1163e775a

Additional Type Guard

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

https://gist.github.com/d44a363b46cb71912bc319f19487a4fc

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, payload: Promise<T> } and transforms it to Action & { meta?: U, payload: T }. This pattern can be used similarly in any middleware situation.

Wrapping Up

My small team at work 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment