- Notifications
You must be signed in to change notification settings - Fork0
microstates/union
Folders and files
| Name | Name | Last commit message | Last commit date | |
|---|---|---|---|---|
Repository files navigation
Model state machines using disjoint union types.
What makes a state machine so useful is that it must always be inexactly one of a set of discreet states. For example, consider thefollowing state diagram for a machine that models the states of a Promise.
A promise starts in thePending state, and then, it either moves totheResolved, or theFulfilled state. But however it ends up, atany given point in time itmust be one of those three states.
Union types (also calledenum types in languages likeJava andTypeScript) have this exact same property. While they allrepresent a single abstract data type, each particularvalue thatyou hold a reference to must be an instance of exactly one concretesubtype.
If we were to imagine a type hiearchy that implemented a scheme likethis, it would look something akin to the following with an abstractsuperclass ofPromise that has three concrete constructorsPending,Fulfilled, andRejected:
We can never instantiate an instance ofPromise directly, but instead mustalways hold a reference to exactlyone of its subtypes. Just like astate machine! In fact, you can modelany state machine as a union type, wherethere is exactly one concrete constructor for each discreet state inthe state machine.
Furthermore, you can model each state transition from one state toanother as amethod on the type representing the first state thatreturns an instance of the type representing the second.
Let's pencil in those methods representing these state transitionsinto our type hierarchy.
As you can see, thePending type has two methods,resolve() andreject(). Theresolve() method returns an instance ofFulfilled,and thenreject() method returns an instance ofRejected.
Notice how it's impossible to transition fromFulfilled toRejected or fromFulfilled toPending because there is no methodto do so.
You can assemble hiearchies like this on your own, but given howcommon it is to model state machines with types,@microstates/unionprovides theUnion helper to assemble it quickly for you.
TheUnion function takes a set ofname: Function pairs and returnsthe abstract union type, where thename is the name of the state,andFunction is a function that takes the abstract superclass as aparameter and returns a subclass that extends that superclass.
We can use it to define our promise type hierarchy.
importUnionfrom'@microstates/union';constPromiseType=Union({Pending:PromiseType=>classextendsPromiseType{},Resolved:PromiseType=>classextendsPromiseType{},Rejected:PromiseType=>classextendsPromiseType{}});
The abstract type will have all of its concrete types attached to it
typeofPromiseType.Pending//=> 'function'PromiseType.Pending.prototypeinstanceofPromiseType//=> truetypeofPromiseType.Fulfilled//=> 'function'PromiseType.Fulfilled.prototypeinstanceofPromiseType//=> truetypeofPromiseType.Rejected//=> 'function'PromiseType.Rejected.prototypeinstanceofPromiseType//=> true
You can instantiate a member of the union using the staticcreatemethod. Every member has boolean properties to help you determine whatkind of state it is:
letpending=PromiseType.Pending.create();pendinginstanceofPromiseType//=> truependinginstanceofPromiseType.Pending//=> truepending.isPending//=> truepending.isRejected//=> falsepending.isFulfilled//=> false
Anything passed to the create method will be used as thestateproperty of the microstate
letfulfilled=PromiseType.Fulfilled.create('result');fulfilled.isPending//=> falsefulfilled.isFulfilled//=> truefulfilled.state//=> 'result';
In order to add transition methods to a member, just add them to itsclass declaration. Here's how we'd add theresolve() andreject()method. Every union type has a privateto[Member] helper method tohelp you transition between states.
importUnionfrom'@microstates/union';constPromiseType=Union({Pending:PromiseType=>classextendsPromiseType{reject(reason){returnthis.toRejected(reason);},resolve(result){returnthis.toFulfilled(result);}},Resolved:PromiseType=>classextendsPromiseType{},Rejected:PromiseType=>classextendsPromiseType{}});letpending=PromiseType.Pending.create();letfulfilled=pending.resolve('here is some data');fulfilled.isFulfilled//=> truefulfilled.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 thenormal way in which microstate values are stored internally. However,storing this type internally is necessary since a microstate tree mustalways be computable from its value.
import{valueOf}from'microstates';letfulfilled=PromiseType.FulFilled.create('data');valueOf(fulfilled)//=> { type: "Fulfilled", value: "data" }
In order to deserialize a union type, you should pass the{ type, value } combination:
letrejected=create(PromiseType,{type:"Rejected",value:newError('something went wrong!')});rejected.isRejected//=> truerejectedinstanceofPromiseType.Rejected//=> truerejected.state//=> Error { "something went wrong!" }
About
@microstates/union - Use disjoint union types to model state machines
Resources
Uh oh!
There was an error while loading.Please reload this page.
Stars
Watchers
Forks
Releases
Packages0
Uh oh!
There was an error while loading.Please reload this page.
Contributors2
Uh oh!
There was an error while loading.Please reload this page.


