Improved Redux type safety with TypeScript 2.8

Japan powder, there is nothing better in this world. Hai!
UPDATE — August 2018
I’ve released rex-tils library, which includes solution discussed within this article. Check it out!
Disclaimer:
This post was created 35000 feet above the ground in the sky, during my return flight from awesome Japan and yes that guy in the title picture is me, ridding the best powder on the planet in Japan :D ( sorry I just had to do this, you can hate me later ok? / later === after reading this post )

Some time ago I wrote about how to effectively leverage TypeScript to write type-safe action creators and reducers.

At that time ( pre TS 2.8 era ) there wasn’t a clean solution how to get the return type of an function without some hacks.

Before Typescript 2.8

You had to do it like this:

  • define type definition first,
  • then implement action creator with return type annotated with your defined type
  • when you changed one thing, you needed to manually update other and vice-versa, bleh…
// you need to define the shape first
type SetAgeAction = { type: typeof SET_AGE; payload: number }
// then implement the shape
const setAge = (age: number): SetAgeAction => ({ type: SET_AGE, payload: age })

Thanks God ( Anders ), that is past now…


With latest additions to TypeScript 2.8, we can get return type of any particular function definition! Let’s see how and why is it important for our action creators et al… shall we ?

Predefined conditional mapped types within standard library (lib.d.ts)

TypeScript 2.8 supports conditional types, which is a huge addition to the type checker! ( kudos to Mr. Anders Hejlsberg ).

I won’t get into details about them in this post as it deserves post on it’s own for sure. ( you can read more here )

Thanks to this addition, we can create new powerful mapped types, let’s say, for getting return type of a function, which we desperately need for our action creators.

What are mapped types? Read more here

Wait a second! It turns out we don’t have to create anything at all, ’cause they are already a part of TS 2.8! We’ve got following new mapped types at our disposal:

  • Exclude<T, U> -- Exclude from T those types that are assignable to U.
  • Extract<T, U> -- Extract from T those types that are assignable to U.
  • NonNullable<T> -- Exclude null and undefined from T.
  • ReturnType<T> -- Obtain the return type of a function type.
  • InstanceType<T> -- Obtain the instance type of a constructor function type.
for more info (check out this PR)[https://github.com/Microsoft/TypeScript/pull/21847]

We will focus only on ReturnType<T> mapped type, which will get us return type of an action creator(a javascript function) which we have been all looking for! Let's see how.

ReturnType<T>

Let’s define our action type and action creator for setting a user age

Now we need to get somehow the return type of our FSA creator.

FSA = Flux Standard Action
Why we need the return type of action creator?
We wanna leverage discriminant unions for 100% type safe reducers and for handling side effects via epics (redux-observable) or effects (@ngrx/store)

So as I’ve already mentioned billion times until now ( sorry :D ), we can leverage new mapped type ReturnType<T>:

Why typeof setAge ?
We need to provide a type annotation as a generic argument to ReturnType mapped type.
We can actually use a variable in a type annotation using the typeof operator. This allows you to tell the compiler that one variable is the same type as another.
In our case we are capturing the type annotation of our setAge function

That’s it! Is it!?

…well when we look at the inferred implementation of SetAgeAction, we will see following type definition

{
type: string
payload: number
}

Oh no, where did our const type literal go? TypeScript flattened it to a string literal base type, which is string. We need to be explicit within our action creator, to make the proper type annotation flow correctly.

Fix is easy enough — we need to explicitly cast it to our SET_AGE literal type, again with the help of typeof operator:

Now our inferred type is correct

{
type: '[user] Set Age'
payload: number
}

PROS:

  • action type is inferred from implementation and because of that stays in sync!

CONS:

  • explicitly casting type property within action creator

Reducing the action boilerplate (createAction)

I don’t know about how you, but I don’t like to cast type property explicitly within every action creator! Bah! Huh!

Let’s write a super tiny utility function for creating our FSA action object:

createAction helper via TS function overloads
We are using typescript function overloading so we get proper types by argument arity

Now we can define our action creator like this:

And our inferred action type remains the same

type SetAgeAction = ReturnType<typeof setAge>
// type
{
type: '[user] SET_AGE'
payload: number
}

PROS:

  • ( as before ) action type is inferred from implementation and because of that, it stays in sync!
  • we don’t have to cast our type
  • more concise than before

CONS:

  • none I guess ? :)

Reducing the action boilerplate even further (action)

We can push it even further by creating custom action helper.

Eeeeh an action helper ?

action helper via TS function overloads and generics
again we are using function overloads to get proper action types

Which we can use like this:

using action helper for creating type safe action creator

with getting our inferred action type definition as before

type SetAgeAction = ReturnType<typeof setAge>
// type
{
type: '[user] SET_AGE'
payload: number
}

or if our action doesn’t contain any payload, TypeScript can properly infer the type literal from it’s source, without providing generics explicitly ( yay ):

With that our DoSomethingAction will have following type:

{
type: '[misc] do something'
}

PROS:

  • ( as before ) action type is inferred from implementation and because of that, it stays in sync!
  • ( as before ) we don’t have to cast our type
  • even more concise than createAction

CONS:

  • if we wanna provide payload, we have to explicitly declare typeof SET_AGE as 1st generic argument, because TS isn't able to do that for us properly ( it will flatten string literal to just simple string)

Reducing the action boilerplate by using classes

As I’ve mentioned in my previous article, you can create action creators via classes, which is, even after the ReturnType<T> conditional mapped type addition, IMHO the most concise way to create actions.

Eat this:

Both Action Creator and Action type definition in one — pure class

Everything is defined once -> implementation and action type definition. Elegant don’t you think ?!

Only “downside” is that traditional redux won’t allow you this approach as actions need to be POJOs = objects with no prototype chain.
This can be mitigated via custom middleware, which shallow copies created instance to pure Object.
export const actionToPlainObject: MiddlewareFn<{}, Action> = store => next => action => next({ …action })
If you’re using @ngrx/store you don’t need to do this, because it’s only restriction is to be an serializable object

PROS:

  • implementation is also type definition because structural origin of TypeScript Classes
  • concise and elegant

CONS:

  • constructor parameter named generically payload, which may not be very descriptive if payload is a primitive type
  • (if you’re using redux ) you need to provide custom middleware for flattening custom class instance to pure object
  • using new SetAgeAction(18) may feel strange or just wrong to some functional purists, I don't mind personally 😎, it makes it even more visible within component code that I'm creating a FSA

And the winner is

Update: 2018–02–28: created final pattern without classes
Update: 2018–03–11: added final pattern example with string enums and declaration merge under one token for even less boilerplate

So until now I’ve preferred and used action creators created via classes. Thanks to Igor Bezkrovnyi comment, I’ve revisited and got following ultimate solution working! →

It uses first boilerplate reduction pattern — the createAction function helper.

Our action types and action creators with Action Union type for reducer ( leveraging discriminant unions ) →
all action types, Action creators, Actions union type for discriminant union usage in reducer

or if you prefer string enums ( which is not valid JavaScript construct ):

all action types, Action creators, Actions union type for discriminant union usage in reducer leveraging string enums
Wonder what that ActionsUnion custom mapped type is ?
It just leverages our new best friend ReturnType<T> and returns all action types union type, even less typing for us! yay!
ActionsUnion mapped type helper
Wonder why both action creators implementation and action types union are named the same — > Actions?
TypeScript supports type declaration merging, which we are leveraging to full extent.

Thanks to this feature, we have united implementation and Discriminant type unions of our implementation under one token, which reduces the boilerplate even more, whenever we’re importing our actions to reducer, to component for dispatching or to epic for filtering.

Actions union type and implementation merge under one token
Leveraging Actions union type and implementation merge under one token

With that said, let’s grab some beer and popcorn and enjoy your 100% type-safe reducer:

Summary

TypeScript 2.8 comes with very important new features:

  • conditional types
  • new default conditional mapped types, from which we can leverage ReturnType<T> to get return types of a function

Thanks to these new features, TypeScript is able to infer action type of our action creator implementation, so we don’t have to duplicate our work and keep type definition and implementation in sync.

Instead of telling the program what types it should use, types are inferred from the implementation, so type checker gets out of our way!

That’s it!


As always, don’t hesitate to ping me if you have any questions here or on twitter (my handle @martin_hotell) and besides that, happy type checking folks and ‘till next time! Cheers!

Like what you read? Give Martin Hochel a round of applause.

From a quick cheer to a standing ovation, clap to show how much you enjoyed this story.