Skip to content

Latest commit

 

History

History

fsm

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 

Finite state machine (FSM)

Store the current state of a stepwise operation.

A finite state machine is a datatype that represents a directed graph, where the graph nodes are a finite set of states, and the graph edges are transitions between those states.

Here is how you define a state machine with state types and the transitions between states.

const water = createFsm<{
  solid: void,
  liquid: void,
  gas: void,
}>()
  .transition('melt', { from: 'solid', to: 'liquid' })
  .transition('boil', { from: 'liquid', to: 'gas' })
  .transition('condense', { from: 'gas', to: 'liquid' })
  .transition('freeze', { from: 'liquid', to: 'solid' });

Infer a union of all possible state instance types from the FSM.

type Water = InferFsmStates<typeof water>;

Create an initial FSM state instance.

let puddle: Water = water.start('solid');
puddle.state; // solid

Use the transition methods to traverse the different states.

puddle = puddle.transitions.melt();
puddle.state; // liquid

puddle = puddle.transitions.boil();
puddle.state; // gas

FSMs and FSM state instances are immutable. The start() method and all transition methods return new state instances, which must be assigned to a variable or they will be lost.

In the above example, the new state is always re-assigned to the let puddle: Water variable. The variable type begins as type Water which is a union of all the state types. TypeScript is smart enough to narrow the type when you use start(), a transition method, or check the state property value.

The above states are all value-less (void), but each state can have a value associated with it.

const bus = createFsm<{
  parked: void,
  driving: { passengers: number, speed: number },
  stopped: { passengers: number },
}>()
  .transition('go', { from: 'parked', to: 'driving' });
  .transition('go', { from: 'stopped', to: 'driving' });
  .transition('go', { from: 'driving', to: 'driving' });
  .transition('stop', { from: 'driving' to: 'stopped' });
  .transition('park', { from: 'driving', to: 'parked' });

type Bus = InferFsmStates<typeof bus>;

let bus1: Bus = bus.start('parked');

bus1 = bus1.transitions.go({ passengers: 0, speed: 15 });
bus1 = bus1.transitions.stop({ passengers: 5 });
bus1 = bus1.transitions.go({ passengers: bus1.value.passengers, speed: 25 });
bus1 = bus1.transitions.go({ passengers: bus1.value.passengers, speed: 55 });
bus1 = bus1.transitions.go({ passengers: bus1.value.passengers, speed: 25 });
bus1 = bus1.transitions.stop({ passengers: 2 });
bus1 = bus1.transitions.go({ passengers: bus1.value.passengers, speed: 25 });
bus1 = bus1.transitions.stop({ passengers: 0 });
bus1 = bus1.transitions.go({ passengers: bus1.value.passengers, speed: 15 });
bus1 = bus1.transitions.park();

Subsets of the state instance types can also be inferred, which can be useful for creating functions which accept specific states.

type ParkedOrStoppedBus = InferFsmStates<typeof bus, 'parked' | 'stopped'>;
type DrivingBus = InferFsmStates<typeof bus, 'driving'>;

function startDriving(bus: ParkedOrStoppedBus): DrivingBus {
  return bus.transitions.go({ passengers: 0, speed: 5 });
}