Unfortunately, the amount of boilerplate required for TypeScript to be effective with Redux can be disheartening. In his article Introducing @ngrx/entity, Mike Ryan shows how
@ngrx/entity can be used to write CRUD reducers with little code. It’s great. And much appreciated. However, it doesn’t help with the TypeScript cruft in action declarations.
In the past, I’ve resorted to code generation — using doT — to avoid the usual repetition. More recently, I’ve investigated alternative approaches and I’ve found one that’s terse and suits my needs.
Before I introduce the library I’ve written, let’s look at how TypeScript works with Redux.
TypeScript and Redux
Redux is fundamentally about the dispatch and receipt of actions, and TypeScript has benefits for both.
When dispatching an action, the use of action creators — rather than object literals — is recommended. There are a number of reasons for using action creators — including brevity, encapsulation and testability — but TypeScript offers another: type safety. Strongly typed actions will prevent the omission of required properties, the inclusion of unnecessary properties, and the inclusion of properties that have the incorrect type. However, there’s nothing complicated here: you just create an action using a TypeScript class or method and pass it to
dispatch. It’s where actions are received that problems arise.
In Redux, actions are simple, anonymous objects, so when an action is received, its
type property is all that there is to work with. (If you create an action using a class, there is no guarantee that when it’s received it will still be an instance of that class — it could be an action replayed by the Redux DevTools — so
instanceof cannot be used.)
Typically, the base Redux action will be defined using an interface and will look like this:
When a reducer receives an action like this:
We want to work with it not as an
Action, but as a type that also includes the
id properties. In TypeScript, this is referred to as type narrowing and there are two mechanisms for performing narrowing: type guards; and discriminated unions.
Narrowing with type guards
instanceof type guards, but for Redux actions, a user-defined type guard is what’s required. A user-defined type guard is a function that performs a run-time check to evaluate its returned type predicate.
For example, if we have this interface:
We can write a user-defined type guard to determine whether an
Action is an
AddTodo action. The user-defined type guard looks like this:
Of particular interest is the function signature’s return type:
action is AddTodo. This is the type predicate and it’s what makes the function a type guard.
We can use our type guard to write a reducer like this (the example reducers in this article perform some basic CRUD actions by manipulating arrays; if you are using NgRx, I’d recommend using
TypeScript will recognise the use of the type guard and, inside the
if statement, it will be aware that the
action instance is of the type
AddTodo and will have
Narrowing with discriminated unions
Discriminated unions are best explained by example, so lets create one using these interfaces:
Both are Redux actions, so they each have
type properties, but it’s the types of those properties that are important. The
type properties in the interfaces are not declared as
string; instead, the are declared using distinct, string-literal types:
A property with a string-literal type is not just a string; it’s a string that can only have the specified value. So the
type property in
AddTodo can only have a value of
"ADD_TODO". TypeScript is able to narrow a union of types in which all of the types share a common, string-literal property.
With the interfaces, we can write a reducer like this:
Note the type of the reducer function’s
AddTodo | RemoveTodo. It’s a union type and it tells TypeScript that the action parameter will be either
RemoveTodo. With that information — and the with the common, distinct, string-literal
type properties in the interfaces — TypeScript will narrow the type of
action within the
Now that we’ve looked at the two narrowing mechanisms, let’s look at how they are used in two different implementations: NgRx and
Narrowing actions with NgRx
The approach NgRx takes — as illustrated Mike’s article — uses a discriminated union for the narrowing. Classes are used as action creators and their declarations look like this:
The constants associated with the actions’
type properties are declared in an
enum and the
enum is exported, so that its members can be used in reducers.
Actions are declared as classes and the
enum constants are assigned to the classes’
type properties. Because the
type properties are
readonly, TypeScript will infer a string-literal type for the property. This is a key point. If the type properties are not declared as
readonly, TypeScript will widen the inferred type to
string — as the properties could be re-assigned a string with an arbitrary value.
A union type — of all the action classes — is also exported for use in the reducer and the reducer looks something like this:
Narrowing actions with typescript-fsa
typescript-fsa is a small action-creator library for Flux-standard actions — which have this shape:
It takes a different approach to NgRx and uses user-defined type guards to perform the narrowing. Instead of using classes as action creators,
typescript-fsa uses functions that accept anonymous objects that have a specified shape. And those action creator functions are created using a factory, like this:
And, in the reducer, the narrowing is performed with the
isType function — which is a user-defined type guard:
Narrowing actions with ts-action
The approach taken by
typescript-fsa involves less boilerplate than that taken by NgRx. However, there were a number of reasons why I was reluctant to adopt
- the actions I’d been using didn’t have the shape of Flux-standard actions — it’s common in Redux to add properties at the same level as the
typeproperty, rather than under a
- all of my reducers used
I just wanted to eliminate the code-generation in my projects, as it complicated the build process. I didn’t want to have to change the action structure or re-write the reducers.
Instead, I wrote a library —
ts-action — that takes a different approach and can narrow using either type guards or discriminated unions.
ts-action uses classes as action creators. However, those classes are created through calls to its action method, so the action creator declarations look like this:
props method will place the specified properties at the same level as the
type property. To place them inside a
payload property, the
payload method can be used instead.
ts-action also includes
base method that allows for the base class to be specified inline, like this:
Inline base classes offer flexibility for property defaults and initialization, etc.
The classes created by
ts-action explicitly reset each action’s
prototype, so that they are compatible with
reactjs/redux — that is, so that each action is considered to be a plain object.
The action creators can be used in reducers that narrow using a discriminated union, like this:
And they can be used in reducers that narrow using user-defined type guards, like this:
Being RxJS-based, NgRx also includes some methods that act like operators and can be used to filter and narrow an observable of actions. In particular, the
Actions observable used with
@ngrx/effects has an
ofType property and it looks like this:
I wanted an operator that could be used with the action creators in
ts-action, so I created
ts-action-operators. It contains an
ofType operator that accepts an action creator:
With all of the information in the action creator, there is no need to specify a type parameter, as the
ofType operator is implemented using the
isType user-defined type guard in
ts-action-operators is independent of NgRx, so it can be used with redux-observable epics, too.
Overall, I’m pleased with
ts-action. With it, I've been able to remove a reasonable amount of boilerplate, and, building it, I’ve learned rather a lot about some of TypeScript’s less-often-used features. And what I’ve learned will likely be the subject of my next article.
This post is also published on my personal blog: ncjamieson.com.