Abstracting common patterns in TypeScript
A major challenge while developing an user interface is dealing with the events that are triggered by user interaction. Not only are there often many possible events that can occur at any given time, the order in which events get triggered is also variable (or they can happen simoustanly). Outside of those events JavaScript applications has to deal with other asynchronous code like handling API calls.
During the decades in which JavaScript evolved to where we are today there have been a multitude of ways to write async code. From the event listeners in html, the callback-hell of JQuery and the onChange property of your React components, to just name a few most people will recognize. And in this wild journey there have been some discutable patterns and ad-hoc solutions that turned out to be not as ideal as hoped for.
In this article I want to look a way of dealing with this idea of ‘code that happens when something else has happened’ that has a more formal and mathematical background to hopefully help you get an intuition for the common foundations of all those patterns. This is in no means a replacement for them, but an exposition of a way to logically reason about all of this.
All ways of dealing with events have some conceptual similarities: we have a bit of code that runs after something else happened. In all the examples from the introduction you can see that no-matter which way of dealing with events we use we have two things: something we wait for to happen and something we do after. In terms of imperative code we could say we wait until we continue execution of our code. This is where the continuation get its name from: we continue after something has happened. So what does a continuation look like? Well actually pretty simple. They are nothing more than a function that gets a callback as argument:
/**
* The standaard definition of the continuation monad
* @param callb - the callback that will get invoked when a value is produced
*/
type cont<T> = (callb: (_: T) => void) => void
Note that the long name ‘continuation’ commonly gets shorted to ‘cont’
While this is the correct definition of a continuation I want to use an slightly different one that wraps the type above in an interface. This has the benefit of use being able to use a fluent api for the operations we will define for continuations:
/**
* A continuation monad with it's operators in a fluent api style, enabling fluent programming and easy chaining of methods
*/
export interface Cont<T> {
/**
* Run the continuation by passing the callback
*/
run: (callb: (_: T) => void) => void
}
As you can see, the definition of the run
property is exactly the same as the definition before. Within this type we can add more methods to the Cont
type to define all our operations. But what exactly is a continuation?
With old-skool JavaScript callbacks we are really focussing on the callback: the entire pattern is centered around providing callbacks to async functions that get invoked when those functions are done. With continuations we like to reason the other way around: we have things that will output values at a certain time. The focus here is the thing-that-outputs instead of the logic that happens after. With continuations we will reason and program around this object that has not yet a value, but will eventually have one. For JavaScript this has the additional benefit of not really having to care on how we register the event listeners (something that changes all the time) but instead hide this behind a library of those objects that will eventually contain values. The callback we will provide is now nothing more than a connector between the world of continuations and the rest of our code.
#javascript #typescript #web-development #programming #developer