Model state machines using disjoint union types.
What makes a state machine so useful is that it must always be in exactly one of a set of discreet states. For example, consider the following state diagram for a machine that models the states of a Promise.
A promise starts in the Pending state, and then, it either moves to
the Resolved, or the Fulfilled state. But however it ends up, at
any given point in time it must be one of those three states.
Union types (also called enum types in languages like Java and
TypeScript) have this exact same property. While they all
represent a single abstract data type, each particular value that
you hold a reference to must be an instance of exactly one concrete
subtype.
If we were to imagine a type hiearchy that implemented a scheme like
this, it would look something akin to the following with an abstract
superclass of Promise that has three concrete constructors
Pending, Fulfilled, and Rejected:
We can never instantiate an instance of Promise directly, but instead must
always hold a reference to exactly one of its subtypes. Just like a
state machine! In fact, you can model any state machine as a union type, where
there is exactly one concrete constructor for each discreet state in
the state machine.
Furthermore, you can model each state transition from one state to another as a method on the type representing the first state that returns an instance of the type representing the second.
Let's pencil in those methods representing these state transitions into our type hierarchy.
As you can see, the Pending type has two methods, resolve() and
reject(). The resolve() method returns an instance of Fulfilled,
and then reject() method returns an instance of Rejected.
Notice how it's impossible to transition from Fulfilled to
Rejected or from Fulfilled to Pending because there is no method
to do so.
You can assemble hiearchies like this on your own, but given how
common it is to model state machines with types, @microstates/union
provides the Union helper to assemble it quickly for you.
The Union function takes a set of name: Function pairs and returns
the abstract union type, where the name is the name of the state,
and Function is a function that takes the abstract superclass as a
parameter and returns a subclass that extends that superclass.
We can use it to define our promise type hierarchy.
import Union from '@microstates/union';
const PromiseType = Union({
Pending: PromiseType => class extends PromiseType {},
Resolved: PromiseType => class extends PromiseType {},
Rejected: PromiseType => class extends PromiseType {}
});The abstract type will have all of its concrete types attached to it
typeof PromiseType.Pending //=> 'function'
PromiseType.Pending.prototype instanceof PromiseType //=> true
typeof PromiseType.Fulfilled //=> 'function'
PromiseType.Fulfilled.prototype instanceof PromiseType //=> true
typeof PromiseType.Rejected //=> 'function'
PromiseType.Rejected.prototype instanceof PromiseType //=> trueYou can instantiate a member of the union using the static create
method. Every member has boolean properties to help you determine what
kind of state it is:
let pending = PromiseType.Pending.create();
pending instanceof PromiseType //=> true
pending instanceof PromiseType.Pending //=> true
pending.isPending //=> true
pending.isRejected //=> false
pending.isFulfilled //=> falseAnything passed to the create method will be used as the state
property of the microstate
let fulfilled = PromiseType.Fulfilled.create('result');
fulfilled.isPending //=> false
fulfilled.isFulfilled //=> true
fulfilled.state //=> 'result';In order to add transition methods to a member, just add them to its
class declaration. Here's how we'd add the resolve() and reject()
method. Every union type has a private to[Member] helper method to
help you transition between states.
import Union from '@microstates/union';
const PromiseType = Union({
Pending: PromiseType => class extends PromiseType {
reject(reason) {
return this.toRejected(reason);
},
resolve(result) {
return this.toFulfilled(result);
}
},
Resolved: PromiseType => class extends PromiseType {},
Rejected: PromiseType => class extends PromiseType {}
});
let pending = PromiseType.Pending.create();
let fulfilled = pending.resolve('here is some data');
fulfilled.isFulfilled //=> true
fulfilled.state //=> 'here is some data'Internally, the value of a union type is stored as a { type, value }
object which can be confusing since it does not correspond to the
normal way in which microstate values are stored internally. However,
storing this type internally is necessary since a microstate tree must
always be computable from its value.
import { valueOf } from 'microstates';
let fulfilled = PromiseType.FulFilled.create('data');
valueOf(fulfilled) //=> { type: "Fulfilled", value: "data" }In order to deserialize a union type, you should pass the { type, value } combination:
let rejected = create(PromiseType, { type: "Rejected", value: new Error('something went wrong!')});
rejected.isRejected //=> true
rejected instanceof PromiseType.Rejected //=> true
rejected.state //=> Error { "something went wrong!" }

