Monads simplified with Generators in TypeScript: Part 1

Async functions solve callback hell for the Promise “monad”. Generators solve this for any Monad in TypeScript.

Kasper Peulen
Flock. Community
Published in
7 min readNov 20, 2020

A couple of weeks ago, I decided I wanted to learn Monads in depth. I am interested in functional programming and prefer to keep functions pure. I try to keep variables immutable and use algebraic data types (tagged unions) to model my data in Typescript. Although learning about Monads can certainly be interesting and challenging, I expected them to be of limited use in my day to day work as a TS developer.

This expectation slowly changed. Apparently, I already use “monad-ish” interfaces every day. You too might already chain callbacks in the then method of a Promise. Moreover, you may also chain callbacks with the .map and .flatMap methods of Array. If so, you are already using Monads!

After a workshop about the ZIO library in Scala, (thanks Mark de Jong!), I realized that there are many other useful Monads. They are meant to solve the same problems I encounter every day as a professional TS developer. However, Monads solve them more elegantly than I ever dared to dream about! In the upcoming parts of this series, we will go over Monads such as Reader and Either that solve dependency injection and error handling respectively. All this in a pure, elegant, and typesafe way.

But first, let’s get a deeper understanding of the problem that Promise and and async/await solve in JavaScript and why this similar to the problem that Haskell tried to solve when they implemented Monads in the '90s.

How the Promise monad rescued JavaScript from callback hell

If you have written JavaScript before Promise became standardized in ES2015, the following code will probably look familiar to you. Callbacks are a natural way to handle asynchronous code in JavaScript. In the browser, it allows you to do an HTTP request without blocking new user interactions. In Node, it allows database access without blocking new HTTP requests from being handled.

Passing a callback as a parameter like in the example above solves this problem. Unfortunately, it often becomes so unreadable it is referred to as callback hell.

Luckily, Promise became standardized. The trick is to have a method of the following form:

This allows the code above to be chained using the then method:

Much better! Besides that, async functions were introduced in ES2017. This allows writing code without passing callbacks at all.

For this example, it probably depends on your taste, which style you prefer. If you are comfortable with functional programming, you may actually use the then methods. You could even write it in one line if you are into point-free programming:

However, there are many cases where using the async/await syntax is essential to solving callback hell:

If the variable a is needed in every subsequent callback, then those callbacks need to be wrapped by the callback of a. A similar problem holds for the other variables, which leads again to callback hell. Luckily, async functions can rescue us here:

This is syntactic sugar for the code above, but helps a lot with writing complex asynchronous flows!

What have Promises to do with Monads?

The problem of callback hell is often associated with asynchronous programming in JavaScript. However, in Haskell, they found that this problem has not so much to do with asynchronous programming per se and that similar problems arise whenever you have a structure that they called a “Monad”.

To define the Monad interface, you need higher kinded types, which Typescript doesn’t support. And even if TypeScript would support that, it would miss Haskell’s type classes that allow interfaces to be implemented by existing types, such as a Promise or an Array.

It is possible to imitate those advanced type patterns in TypeScript, but in this post, we will keep it a bit more simple and just see a Monad as a type that has certain properties. We say that a type Monad<A> is monadic for a type A if we can define the following functions:

You can think of this that there is a way to put a value: A inside a monad with the function of . And there is a way to access this value of type A again by passing a callback to the flatMap function.

For a Promise those functions are implemented by Promise.resolve and by the then method:

The Monad laws

In Haskell, a Monad must not only implement the of and the flatMap function, it must also obey the following 3 laws.

The identity laws make sure that of and flatMap work as essentially as inverses of each other.

The left identity law says that whenever you put a value inside a monad with of and access that value again by passing a callback toflatMap, then you will have the same value available in the callback again. For a Promise, you can see that as:

Makes sense? We wouldn’t want to have the then method magically transform the value we are passing, for example, that we put in 3, and get out 4!

The right identity law says that whenever you access the value of a Monad using flatMap, and then put that value in a Monad again with of, you will have back the same Monad.

For a Promise, you can see that as:

Last we have the associativity law:

For Promise’s, you can see this as that it doesn’t matter if you chain then methods inside the callback of another then method, or that you chain it outside of the callback on the resulting promise.

But why?

I hope this gives a sense of why a Promise is Monad-ish, but there is a caveat here. Promises are designed so that they are automatically flattened. You can’t have a Promise inside of another Promise:

But according to the left identity law, x must be equivalent to Promise.resolve(3). The monad rules don’t allow that, and for good reasons. We saw thatasync/await syntax allow for using Promise’s without writing callbacks. In Haskell, you have a similar syntax (called do-notation) that does essentially the same, but then for any Monad! The laws are essential to make such a generalized syntax predictable.

In essence, you could say that Monad-ish structures arise whenever it makes sense to do calculations by accessing the value in a callback instead of using the value directly. Here are examples of Monad-ish structures that are available in TypeScript and the problems they solve:

  • In the Promise monad you use a callback, as if you would access the value directly, it may not be there yet.
  • An Array is a monad where working with callbacks allows transforming the values without mutating an Array in a for loop
  • An Optional is a Monad that isn’t directly available in TypeScript, but you can think of it as being similar to Nullable<T> = T | null where T is not null itself. Here callbacks allow for accessing the value without having to do a manual null check.
  • An Iterable can represent a sequence of infinitely many values in JavaScript. By using callbacks you can transform it into another infinite Iterable without problems while looping over it one by one would take infinite time!

To see those types as Monad’s, you have to implement the of and flatMap function and show that the Monad laws hold. For an Array, the monad functions can be implemented as follows:

We have special async/await syntax only available for Promise’s in JavaScript. Callback hell arises when you compose a Monad from multiple other Monad’s that depend on each other, which is a very common pattern for Promise’s.

However, it is possible to have the exact same callback hell for Array’s. For example, say that we want to construct right triangles with sides a and b, and hypotenuse c.

And there we have callback hell again! To avoid callback hell for any Monad, Haskell came up with a special do-notation syntax that “looks” like accessing the monadic value directly, but is just syntactic sugar for a chain of flatMap’s:

Maya Victor makes a very similar point as I draw here in this post, and says that “if mapping over nested arrays was as common as dealing with asynchronous values, by 2018 we’d probably have an multi/pick syntax”. The multi/pick syntax for Array’s would look like this:

Generators can solve callback hell for any Monad in TypeScript

However, this “imaginary” syntax is actually possible in TypeScript with Generators!

The idea is essentially the same as with async functions. Instead of using await to access the value of type A of Promise<A>, we use the yield keyword to access the value of type B of any Monad<B>.

Using yield directly on a Monad works fine if you don’t care about types. For TypeScript, we need to wrap the Monad in an extra generator function, pick in the example above, to infer the type that is yielded.

this is similar to the solution used in typed-redux-saga

The multi function in the Array example accepts a Generator and will essentially rewrite the generator function as the nested chain of flatMap’s and give back the resulting Array. Because we don’t have higher kinded types in TypeScript to specify a Monad interface, it is not possible to make this syntax automatically work for any Monad in a typesafe way. However, it is possible to manually implement such multi and pick functions Monad-by-Monad, so that you can use similar typesafe Generator syntax for any Monad.

In the next part of this series, we will go over how Generator’s exactly work. This Generator syntax may not often be useful for Array’s, but it helps a lot for the Reader monad (which solves dependency injection using pure functions) and the Either monad (which helps with doing error handling in a typesafe way). For those two monads, the Generator syntax makes sense for the exact same reason it makes sense for Promise’s: you often need to compose one Monad from multiple other Monads that depend on each other.

In the meantime, if you are interested in the implementation of multi and pick above you can check how it was implemented in the following code sandbox:

There is also a ZIO inspired library available for TypeScript that tries to imitate higher kinded types in TypeScript which has Generator syntax available for Monads specified in that way:

I hope you learned something new, and until next time!



Kasper Peulen
Flock. Community

Programmer, fullstack, loves learning new things.