Finally, the TypeScript + Redux/Hooks/Events blog you were looking for.

Shane Osbourne
HackerNoon.com
11 min readMay 8, 2019

--

Just want the code? https://codesandbox.io/s/jpj18xoo85

We write event-driven applications, even if we don’t realise it at first, any application of decent size will tend towards being powered by events, it’s inevitable. Whether it’s a large-scale Redux architecture, React hooks like ‘useReducer’, NgRx in the Angular world, web sockets, custom-rolled systems etc, etc, etc. The list could go on forever.

Whilst an event based-architecture can lead to your programs having amazing properties in the form of ultra de-coupling & composition, it also comes with it’s own hidden cost — events by their nature are abstract and therefor we don’t typically get the best level of help from our IDE’s when it comes to creating, consuming & filtering streams of them.

So, What’s the problem?

To be productive in a large codebase that has any part of it powered by events, you need `type` information. There’s nothing more infuriating than listening for an event by using a string identifier, only to find out 3 hours later that there was a typo in the event name, or that an expected payload from an event didn’t match what you can actually see in your debugger! 😖

We need to start thinking of ways, especially in these dynamic languages, of mapping out every single event that our application can produce so that we
can handle them safely.

This is crucially important, you absolutely need to be able to verify that events are created correctly, with valid identifiers, and if they take one — a valid payload.

Prior Art, events as individual interfaces.

I’ve tried a number of different ways, and I’ve seen a million others, but the most common amongst them seems to be a variation on the following:

This approach lends itself nicely to being tucked away in its own file, a nice little ‘events.ts’ if you will. Now initially I agree it seems elegant at first, but since the types are written separate to the code that uses/consumes them, it requires a human to keep it up to date and make sure typos don’t exist.

Also, how would you use these when dealing with an event stream? For example, if you had a switch statement, how could you know inside each ‘case’ statement which payload you’re dealing with? With individual interfaces like this, the only option is to export a separate type that’s a union of all possible event shapes.

Such niceness… so tempting…

But now you’ve just created another thing to maintain! GRRR! and when it’s 20 events and you’re scrolling down the file to add your next event to this list, you might just be ready to throw your monitor out of the window. 📺

There’s also so much duplication — the fact that `SignIn` & `SignOut` are duplicated in some fashion across the interface naming scheme and the required ‘type’ field seems excessive — there has to be a better way?

Such cry :(

But it gets worse, because these are just ‘types’, we need to either create functions for each to create a type-safe way of raising these events, or annotate objects at the point of emitting the event. The following shows the former, I’m omitting the latter for my own sanity.

This example is good in the type-safety sense. You have a guarantee on the function parameters, and since the function has a return-type annotation the type-checker will ensure the ‘payload’ here not only has the correct members but also that the parameters were valid to be used in that context (eg: a string where a string is expected)

But it’s far from good, we’ve also now duplicated the parameter names!

We have to remove some of this noise. Type-safety is nice, but this is coming at the cost of developer sanity! Also I believe examples like this are a prime reason that certain seasoned JS devs are so vocal about being anti-types — who can blame them?

A Step Forward, inferring the interface from the return type.

After looking at the code above, you’d be correct in thinking that you don’t even need the interface at all…

2.5 billions times better. But still not good

Thanks to conditional types being added to TypeScript back in 2.8, we now have useful helpers like ReturnType<T> which does exactly what it says on the tin — it infers the return type of a function if it can.

If you’re interested in type systems (you’re not?, how have you gotten this far?) then it’s worth a quick look at ReturnType<T>

It’s just a ternary operator, but for types, how neat! With a bit of practice you get good at being able to visually ‘parse’ this type of code :)

So, this is better than having a separate interface for the event + a function to create it. Now the type is driven by the implementation, not the other way around — this means you can change the function parameters & the return payload, and everywhere you’ve used that type will get the new type checking information.

A major downside though, is that you’d still need to maintain a list of them and union them together to get the full feature set of type safe events.

Getting better, but you still need to union the inferred types together.

The keen-eyed amongst you may notice I’ve just used string literals in these screen shots and that the type-narrow may not actually work correctly — this is for simplicity & to prevent bringing in enums/consts in these examples that I’m actually encouraging you not to use. 😂

On the right track, getting Typescript to do more, with less.

So I’ve played around with numerous variations of the previous example in different sized applications, and overall it was OK. What’s pushed to me demand more though, is the work I’ve done in other languages like Rust & Elm.

It just hit me like a ton of bricks one day, the answer for a better way to type events in Typescript lies in it’s ability to model ADT’s (algebraic data types).

Let’s look at how you’d provide type information for the SignIn + SignOut events in Elm & how you’d create a union of them.

Erm, is that it?

Seriously how cool is this? It’s just single identifiers SignIn & SignOut (they are type constructor in Elm) and some associated data — in the case of SignIn it’s 2 strings to represent the username and password (there are other, more descriptive ways, but this is not an Elm tutorial 😝).

In Rust it’s a similar story, there’s a little bit more syntax noise, but it’s ultimately the same thing.

Rust enums are the biz

It’s perfection. It’s just identifiers, and optional associated data. No more, no less. I needed to have this in TypeScript.

Challenge accepted

The goal is clear, a way to model event identifiers + optional payloads, with as small a footprint as possible. It should be like code-golfing, but with the goal being greater type-safety & developer productivity.

Looking at the rust implementation (since that has curly braces so it’s a fairer comparison 🤣) I wondered just how close I could get to it.

The first few attempts included using an actual JS object and having the ‘event creator’ function as the value to each identifier.

decided it was an anti-pattern after all

I was convinced for the longest time that this was a great approach. The idea would be that you’d define this object, and then call another function with it that would augment the return values to ensure they fulfilled the type + payload contract (so that they returned {type: …, payload:…} )

I put a considerable amount of effort into making this approach work with safety guarantees at all points, but I had another realisation when reviewing some of the large codebases I’ve worked on that were full of events — over 90% of the function I was using just forwarded on the params to the payload — they served no other purpose. In fact, in the few cases I found myself doing ‘work’ in those functions, it could easily be extracted out.

Moving to a types-only approach

Since Typescript supports object literal types, we can even closer to that beautiful world of Rust & Elm with the following:

Pretty damn close to the Rust equivalent

If you squint, you’ll see we’ve reduced everything down to just the essentials — the identifier & the associated data. Now it did take me an embarrassingly long time to come to this solution, mainly because I was completely hindered by this notion that the events needed a ‘creation’ function.

Even though this does absolutely nothing yet, I knew this was an amazing idea.

Making it useable

Of course, that’s just a type definition right now, it’s not exactly ready to be deployed in a giant Redux Application now is it.

But just wait, with a few fancy Typescript feature and a single JS function, you’re about to witness the holy grail of working with events in Typescript.

#1 Switch to an enum for the identifiers

This is a no-brainer, and was only omitted from previous examples to reduce concepts — but since we want a single way to create and consume events, we’ll want to switch out the string keys for an enum — this is also a great way to name-space events.

Starting to look like something useable

Using string enums here is the perfect solution to our ‘identifier’ problem, since we can use the enum members as the discriminant (common property) but also it gives us the flexibility to implement pseudo namespaces in the sting values if we wish (or suffixes/prefixes etc).

There’s also an weirdly nice knock-on effect of this approach in that it will ensure the string values don’t collide (which can happen in enums), although only when the types on the ‘rhs’ differ, so it can’t be relied upon fully 😀

The whole point here though, is that throughout the application, we only ever would use the enum to reference this event —no extra functions or separate interfaces in sight.

#2 A way to create type-safe events

So we have the identifier & associated data part nailed, now we just need a single wrapper function so that we can apply some Typescript magic. We are going to essentially create a function that takes the enum member as the first param, and the associated data as the second. Beautifully simple.

Let’s look back at a Rust example for a second. This snippet will create the SignIn variant with it’s required data (I’ve skipped some rust-specifics here for brevity) Notice how the creation of this is almost identical to it’s definition.

this.is.the.goal

Can we do this in Typescript? With full type safety? This is what I came up with…. 😂

Hidden away in your types folder

Erm, now you’re thinking, where’s the beautiful or simple bits… Well, this is actually just taking a handful of the newer Typescript features in order to create this elm/rust clone. This code would be written once and then hidden away in a file that you never touch — the point is to enable the following:

elm/rust/ts 💜

The last 2 lines show how to create events in this system, the beauty comes from the fact that you don’t need to dream up function names for each event, but instead you just use the identifier and its data.

Msg here will only accept events from the User namespace (given as the first param, like a type constructor) and the second param would be type-safe based on that enum members value in the type object.

#3 benefits of Discriminated Unions/Algebraic data types

All of this type stuff is completely useless if you cannot also narrow the type of incoming events based on the common property. In our case we’re using enum members as the ‘type’ property so this should be simple enough — we want full type-safety in case blocks like this:

In this example we need to know that when a User.Token event is raised its payload is a string. Likewise we need to know when payloads are absent, or complex types etc etc.

This is where Typescript’s discriminated unions come into play — since we already have type safety at event creation, if we can nail getting type guards like this working, we’ll be onto something huge.

In the screenshot above, we wouldn’t get any type help. If you remember back to the type definitions for our events, we didn’t include the type or payload property — but also this reducer function doesn’t have a type annotation for the action — we’ll fix that now.

total type nerdery

The above, whilst hard to read for those unfamiliar, is just known as a Mapped Type —this is a way fo creating a new type that is based on a transformation of the original type. In our case we had {identifier: value} , so this mapped type will do nothing more than create new types for each key

// before
{identifier: value}
// after
{identifier: {type: identifier, payload: value }

Why do we do this? Because it allows us to create a union type of all possible enum + value combinations, so that when given to something like a switch statement the expected type narrowing occurs.

You’d add the following single line, where Messages here is just that object type that has the enum + associated value.

Type magic — indexing in a object literal type with a union of it’s own keys

The square brackets give it away, you’re indexing into a type, but since keyof produces a union of all the keys, you get a union back. You are essentially indexing an object literal type with a union of its own keys. I know, it’s a bit of a mind bender, but it’s awesome.

The result is that Actions above now has exactly the correct type that enables the super-powerful type narrowing — if you hovered over it in our editor, the type would look like this:

And that’s pretty much it — if you can produce this type, you’re going to get awesome type safety throughout your application.

Conclusion

This has incredible type-safety, a nice api that matches languages with type constructors, has no duplication, uses a single identifier for event definition/creation/consumption and is basically just all-round better for all of your event-based needs.

If there’s any interest in a deep-dives into the more advanced typescript stuff, please request it and I may just write a post for type nerds like me :)

Check out the examples here https://codesandbox.io/embed/jpj18xoo85 — it should make things a lot clearer :)

— -

Like this? If you did, and you find yourself doing any front-end work, perhaps you’d enjoy some of my lessons on https://egghead.io/instructors/shane-osbourne— many are free and I cover Vanilla JS, Typescript, RxJS and more.

--

--