How to get better type checking and code completion support with Typescript in Angular Ngrx reducers or any other Redux-like library

All the code examples in this post are taken from the real world Angular Ngrx Material Starter project in various stages of it’s development

Short recapitulation of Ngrx state library

Ngrx is a state management library which is used to implement one way data flow with centralized state store. Components and services dispatch actions in response to events triggered by user interaction and server communication.

Every dispatched action produces a new application state by triggering corresponding reducer action handler. In the handler we always want to return new object or array to be sure that our new application state is clearly distinct from the old one.

That way we achieve immutability of our state which enables major performance optimizations by Angular (eg by using OnPush change detection strategy) and results in less re-renders and a much faster application.

Providing type information for the reducer

General rule is that the more types we provide the more helpful Typescript compiler gets.

Typed actions

Typed actions are implemented using enum with the list of all supported actions and corresponding action classes. Every action class has type property which references single enum value. In addition, the constructor of the action class defines the type of a payload which is necessary to create the action.

This makes it impossible to make a mistake when creating new actions

Moreover, Typescript supports stuff with a fancy name called discriminated unions which come very handy when implementing Ngrx reducer actions handlers. In plain language, discriminated union is a type of the all action types supported by reducer (eg type TodosActions = ActionTodosAdd | ActionTodosFilter | … ).

This enables Typescript to check the type of payload of the action in corresponding action handler for every of the supported actions even though they are all part of one switch statement.

Typed actions are implemented using action type enum and corresponding action classes with type property and optional typed payload

Typed state

It is very useful to define a type of the state object which is managed by the reducer. This type is useful for defining initial state and for the type of the object returned by our reducer. Typing return value of our reducer is in fact the main ingredient for enabling great Typescript support discussed in this article…

Example of a todos.reducer.ts file with all necessary type declarations for proper type-checking and code-completion support

Creating new state using Object.assign()

To make our reducer useful we have to provide some action handler implementations. Purpose of the action handler is to create and return new state object with changes to relevant properties.

Returning of a new object has additional benefit of making implementation immutable which helps with the performance of our app, especially with change detection.

Let’s focus on the FILTER action which updates a value of the filter property in our state. Long story short, we would have a component displaying a todo list and a filter dropdown to help us display todos in various states like done or active.

In the dark past of Javascript we would have to iterate over every property of a state object to create a shallow copy while simultaneously checking if the object actually hasOwnProperty . Those days are luckily gone!

Today we can simply use Object.assign() ( a courtesy of es2015 ) which will result in the following solution…

Creating new state object using Object.assign()

This works great but what would happen if we tried to use properties which are not specified on our state type? After all, we’re using Typescript and we are interested in the benefits it has to offer…

Objects with additional properties which are not specified in the type of the reducer state will unfortunately NOT produce type errors…

We can make this work by using Typescript generics and specifying a type variable for every parameter passed to Object.assign() . As we will see, this can get quite verbose…

Specifying type variable for every Object.assign() parameter using Typescript generics will yield correct compiler errors but has problems on its own…

Besides being verbose, the provided solution will break every time we add or remove a new function parameter without adjusting corresponding type variable.

Adding a new parameter could result in unsupported properties being stored in our state and removing property will lead to annoying type error ( TS2555 ) because the number of expected arguments will not match the number of actual arguments.

There must be a better way!

Object spread syntax for the rescue

Object spread syntax { ...obj } is a rather new addition to Javascript ( and Typescript ) language. Official documentation says that…

The Rest/Spread Properties for ECMAScript proposal (stage 4) adds spread properties to object literals. It copies own enumerable properties from a provided object onto a new object. Shallow-cloning (excluding prototype) or merging of objects is now possible using a shorter syntax than Object.assign() .

The most important part is that we now have a rather nice and concise syntax for the creating of new objects by shallow merging existing ones.

Replacing Object.assing() with object spread syntax

Follow me on Twitter to get notified about the newest blog posts and interesting frontend stuff

With this solution we can start by spreading original state into a resulting object. We will continue by adding every needed property which has a new value and amounts to the actual state change.

Another option would be to create new object with all changed properties beforehand and spread it after original state as in { ...state, ...newState } . This would achieve the same result and is mostly about subjective preference of a developer even though passing properties directly will result in a little bit shorter code…

So what would happen if we tried to pass properties which are not present in our state type?

Typescript correctly detects unsupported properties and reports them as type errors — Yaaay!

And voila, we simply cannot go wrong. Typescript got our back!

As we can see, the resulting code is short, simple and safe which is pretty great compared to original Object.assign() solution. This solution will work seamlessly whenever we decide to add or remove properties from particular action handler and also in case we have to adjust state type itself.

What about Redux?

Even though this article is concerned mainly with Angular’s Ngrx, the same principles should apply to reducer implementation for any state library which expects reducer to return a new state object. So if you’re using Typescript, then object spread is the answer!

Editor code completion support

Another huge benefit of this approach is the great code completion support from IDEs like Webstorm ( IDEA) or VS Code…

Great Typescript powered code-completion support in Intellij IDEA editor

Code-completion is one of the best productivity related benefits of using Typescript

Imagine a huge code base with many reducers and corresponding state types written by many developers. There is a zero chance that we can know the whole project and remember the shape of every interface by heart.

Code-completion can save us lots of time browsing around, searching and remembering name of that one property that we have to update to fix that annoying bug!

Hooray!

We reached the end. Hope you found these tips useful and you will implement them in Ngrx (or even Redux) code in your projects to get a better type-checking and code-completion experience!

Please support this article with your 👏👏👏 to help it spread to a wider audience 🙏 and follow me on 🕊️ Twitter to get notified about newest blog posts 😉

More examples of this concepts can be found in angular-ngrx-material-starter project, mostly in reducer related code.

Also, feel free to check some other interesting frontend & Angular posts…