As you can see, a quite complex process can be described (and depicted!) with a simple directed graph. It’s easy to reason about, it’s simple to understand, and it’s convenient to analyze for possible mistakes. So let’s move on to the next part — encoding this FSM on the type level.

Type-Level Design

To give you an intiution about type-first approach, I would try to express my thoughts when I tackle the task. You may skip this section if you are familiar with type-level approach and go to the Implementation part.

Let’s think out loud — how can we describe each part of the FSM using types? First of all, the idea of list of states can be expressed using a simple sum type:

Take a look at the transition: it has three clearly visible parts: previous state, next state, and an action which is executed when the transition happens.

If we were to create a plain old JavaScript object (POJO), we would express this idea as a function with three arguments returning an object type with three fields. On the type level it could be expressed in the same manner, using generic types. The only runtime part we need is action, which is a simple function:

Now we can define our transitions. I will be using string[] as the payload, so I can log the transitions:

Please note that the Noop transition is kinda tricky, as it should be pluggable between any two states, resulting in the same output state as was the input state. Thus I made it a generic class with one fixed S type parameter.

For the sake of convenience, I will define a set of “functional constructors” — simple functions which will instantiate out transition classes. You’ll see in a jiffy why they are convenient:

And now we are finally ready to express our first FSM transition from a source state to a target state!

Real-Life Example

—Okay, that’s great, but what does it have in common with real life? — a curious reader may ask, so I would like to show you a real-life example of network requester (aka “fetcher”) using the described approach. Hold tight, as it will introduce some new concepts like IORef and Either monads, as well as it will use my circuit-breaker-monad package!

We begin with the definition of FSM states. Here I won’t be using just a sum of string literals; instead I’ll create simple wrappers which have a runtime tag. This approach is used in circuit-breaker-monad package, and I'll stick to it to make my code style consistent.

Next, we define some interfaces for our runtime data. I will be using an awesome MockAPI to mock some data.

Next I will modify the Transition class to better suit our needs. It should hold an async method which will be called during a transition:

Next, let’s define our transitions to represent the diagram above. Please note that the payload may be different for each transition!

The next part is to define our actual request Promise, required by the circuit breaker package. Here I will use AbortController experimental API to cancel my request after a timeout:

The main “meat” of our code — step function, which generates a Transition given the current state of the FSM:

At last, let’s define a recursive method executeFSM , which will make its usage a bit simpler:

And finally — let’s give it a spin!

You can find the whole example in my public Gist. Please note that due to outdated typeings you’ll need to patch node-fetch 's RequestInit interface to include a signal like this:

Conclusion

I hope this small article helps you to grasp the typelevel approach to finite-state machines. I would be happy to discuss your thoughts, so please ping me back at @ybogomolov in Telegram, @YuriyBogomolov in Twitter or via yuriy.bogomolov@gmail.com.