Type safe message dispatch in TypeScript

TypeScript continues to be amazing. Anders and team are doing an incredible job making the language accessible and at the same time powerful enough to express interesting invariants that can be encoded with conditional and mapping types.

I’m currently working on a workflow toolkit and building it in TypeScript has allowed me to express the message dispatch logic in a type safe way. Putting the pieces together has been a lot of fun so I’m going to outline the pattern in case others find it useful.

The skeleton for a message dispatch loop consists of something that looks like the following

Without types there isn’t much we can say about this function. All we know is that there is some kind of mapping from types of message to functions that take the message and do something. We want to preserve as much of this structure as possible while at the same time adding as much type safety as possible. Fortunately, TypeScript has all the ingredients to make this possible.

First thing we need to do is specify the message types and give the compiler enough information to help us out

In a production environment you would spell out all the details and not leave the payload type unspecified but for demonstration purposes I’m going to leave the payload unspecified because it is easy to extend the pattern to typed payloads. The other thing I need to explain is Eq. What’s going on there is that we are verifying that every message type actually has a message assigned to it. Eq will show up throughout so make sure you understand what is going on. If the types in Eq are not equal then we will not be able to assign true to it and the compiler will tell us the assignment in the block is not possible.

Now that the message type is specified we can use a switch statement and ask the compiler to help us out with the dispatch function

Thanks to TypeScript’s exhaustiveness checking the compiler will complain if we forget to check for all the message types that are in MessageType. The assignment in the default block will have red squiggly lines and it will tell us that never is not assignable to some type we forgot to check so when we try to compile we will get warnings about missing cases.

The messages are now handled properly but handlers is untyped. We can assign a type to it as well by using conditional and mapping types. First thing we need to do is specify the type of the handler function

We are again using the same trick with Eq to verify certain invariants. We want to make sure that the key for the mapping and the actual type of the message match up. If they don’t match up then the assignment will fail during compile time and we will get an error/warning that will tell us something is wrong. So if we tried to assign the key exit to the message WorkMessage the assignment would be incorrect and we’d get a warning/error at compile time.

Now with all those types we can make sure handlers variable in our dispatch function is also correct

This makes everything type safe. If you know of a better way to structure the dispatch logic and types then let me know. I’m pretty happy with this solution but open to a better design. I kind wish I could abstract the pattern further but I’m pretty sure that requires higher order types.