finestate
v1.2.0
Published
Powerful and easy to use Finite State Machine with hierarchy, orthogonal states and 100% tests coverage.
Maintainers
Readme
finestate
finestate is a flexible and extensible Finite State Machine (FSM) library for TypeScript. It helps you model and manage stateful logic in a clear, maintainable way. Whether you're building game logic, complex UI flows, or any state-based application, finestate provides a structured approach with support for hierarchical states, parallel (orthogonal) states, and event-driven transitions. The library is lightweight and general-purpose, making it suitable for a wide range of domains.
Quick Usage
Get started quickly by defining your states as classes and specifying how they transition on events. Below are a couple of simple examples to illustrate usage.
Example 1: Toggle between two states
Imagine a simple on/off switch that toggles state on a "toggle" event:

import { EventType, Fsm, State, stateDesc } from 'finestate';
class OffState extends State {
// Entry hook (called when state is entered)
init() {
console.log('Entering Off');
}
// Event handling
processEvent(evt: EventType) {
if (evt === 'toggle') {
// Transition to OnState
// You must return on transit() immediately
return this.transit(OnState); // it will return 'event handled'
}
// You may skip returning false and return nothing to specify the event is not processed
// return false; // event not handled
}
// Exit hook (called when state is exited)
destroy() {
console.log('Exiting Off');
}
}
class OnState extends State {
init() {
console.log('Entering On');
}
processEvent(evt: EventType) {
if (evt === 'toggle') {
return this.transit(OffState); // Transition back to OffState
}
}
destroy() {
console.log('Exiting On');
}
}
// Set up the FSM with OffState as the initial state and OnState as an
// alternative
const fsm = new Fsm([
stateDesc(OffState), // initial state
stateDesc(OnState) // another possible state at the same level
]);
fsm.init(); // initialize the state machine (enters OffState)
// Dispatch events to the state machine
fsm.dispatch('toggle'); // triggers transition from OffState to OnState
fsm.dispatch('toggle'); // triggers transition from OnState back to OffStateIn this example, we define two state classes OffState and OnState by extending the base State class. The processEvent method in each class handles the "toggle" event by transitioning to the other state. We create an Fsm with OffState as the starting state. Calling fsm.dispatch('toggle') causes the FSM to switch states and call the appropriate entry/exit hooks (init/destroy).
Example 2: Hierarchical & Parallel states in a game menu
Consider a simple game flow: the game begins with a splash screen, moves to a loading phase, and then enters a main menu. When resources loading is finished, the game goes to Loaded state. It has two parallel regions: one for the menu UI (which can be active showing either the main menu or a settings screen, or inactive/hidden) and another for the game content (either no game running or a game in progress). We'll use string events like "begin", "ready", "play", "quit", "openMenu", "closeMenu", "settings", and "back" to transition between these states, but in the real life you would use some other markers, like all resources loaded or button clicked:

import { EventType, Fsm, State, stateDesc } from 'finestate';
// Root states
class Splash extends State {
init() {
console.log('Showing splash screen.');
}
processEvent(evt: EventType) {
if (evt === 'begin') {
return this.transit(Loading);
}
}
destroy() {
console.log('Leaving splash screen.');
}
}
class Loading extends State {
init() {
console.log('Loading game resources...');
}
processEvent(evt: EventType) {
if (evt === 'ready') {
return this.transit(Loaded);
}
}
destroy() {
console.log('Loading complete.');
}
}
class Loaded extends State {
init() {
console.log('Initialize resource shared to Menu and Game states.');
}
// Loaded may handle global events if needed (none in this example)
}
// Menu region states (under Loaded)
class MenuActive extends State {
init() {
console.log('Menu is now active.');
}
processEvent(evt: EventType) {
if (evt === 'closeMenu') {
return this.transit(MenuInactive);
}
}
destroy() {
console.log('Menu deactivated.');
}
}
class MenuInactive extends State {
init() {
console.log('Menu is inactive.');
}
processEvent(evt: EventType) {
if (evt === 'openMenu') {
return this.transit(MenuActive);
}
}
}
class MenuMain extends State {
init() {
console.log('Show main menu screen.');
}
processEvent(evt: EventType) {
if (evt === 'settings') {
return this.transit(MenuSettings);
}
}
destroy() {
console.log('Hide main menu screen.');
}
}
class MenuSettings extends State {
init() {
console.log('Show settings screen.');
}
processEvent(evt: EventType) {
if (evt === 'back') {
return this.transit(MenuMain);
}
}
destroy() {
console.log('Hide settings screen.');
}
}
// Game region states (under Loaded)
class NoGame extends State {
processEvent(evt: EventType) {
if (evt === 'play') {
return this.transit(Game);
}
}
}
class Game extends State {
init() {
console.log('Init resource for the game.');
}
processEvent(evt: EventType) {
if (evt === 'quit') {
return this.transit(NoGame);
}
}
destroy() {
console.log('Destroy resources for the game.');
}
}
// Set up the hierarchical state machine with parallel regions in Loaded
const fsm = new Fsm([
stateDesc(Splash), // initial root state
stateDesc(Loading),
stateDesc(Loaded, [
// First child region (Menu): default MenuActive with its sub-states
stateDesc(MenuActive, [
stateDesc(MenuMain),
stateDesc(MenuSettings)
]),
stateDesc(MenuInactive)
], [
// Second child region (Game): default NoGame
stateDesc(NoGame),
stateDesc(Game, [
// Other game substates could be listed here
])
])
]);
fsm.init(); // enters Splash state initially
// Example event sequence:
fsm.dispatch('begin'); // Splash -> Loading
fsm.dispatch('ready'); // Loading -> Loaded (enters MenuActive/MenuMain and NoGame)
fsm.dispatch('settings'); // MenuMain -> MenuSettings
fsm.dispatch('back'); // MenuSettings -> MenuMain
fsm.dispatch('play'); // NoGame -> Game (game starts, menu still active)
fsm.dispatch('closeMenu'); // MenuActive -> MenuInactive (menu hidden, game still running)
fsm.dispatch('openMenu'); // MenuInactive -> MenuActive (menu shown again, enters MenuMain)
fsm.dispatch('quit'); // Game -> NoGame (game ended, menu still active)In this example, the FSM starts in the Splash state. A "begin" event causes a transition to Loading, and when loading is complete a "ready" event transitions into the Loaded state.
The Loaded state has two orthogonal child regions running in parallel: a Menu region (MenuActive vs MenuInactive) and a Game region (NoGame vs Game). Upon entering Loaded, both regions are initialized: the menu region starts in MenuActive (which itself enters its default substate MenuMain), and the game region starts in NoGame. A "settings" event triggers a transition from MenuMain to MenuSettings (within the menu region), and a "back" event returns from MenuSettings to MenuMain. Independently, a "play" event in the game region transitions from NoGame to Game. The menu can be toggled off with "closeMenu", transitioning from MenuActive to MenuInactive while the Game state remains active. An "openMenu" event brings back the menu by transitioning MenuInactive to MenuActive (entering MenuMain by default), and finally a "quit" event from Game transitions back to NoGame. Throughout these transitions, the init and destroy methods log the state changes. This demonstrates a hierarchical state machine with parallel regions: the menu and game states operate concurrently under the Loaded parent state.
Installation
Install finestate via npm (or Yarn):
npm install finestate
# or with yarn:
yarn add finestateThe package includes type declarations and can be used in both Node.js and browser environments. It supports both CommonJS and ESModule imports:
ESM Import (TypeScript/modern JS):
import { Fsm, State } from 'finestate';CommonJS require:
const { Fsm, State } = require('finestate');
No additional configuration is needed – just install and import.
Additionally, finestate provides a UMD (Universal Module Definition) build, suitable for older browsers or scenarios where you want to load the library directly via a <script> tag without using a module bundler. In this setup, a global variable named FineState is exposed on the window object, allowing you to access Fsm, State, and other exports. This can be useful for quick demos, legacy codebases, or any environment that does not yet support modern bundling or module imports. For example:
<script src="finestate.umd.js"></script>
<script>
...
// Now we can create an FSM using the global FineState object
const fsm = new FineState.Fsm([
FineState.stateDesc(MyState)
// ... other states ...
]);
fsm.init();
fsm.dispatch('start');
</script>Core Concepts
finestate employs a class-based approach to define FSMs. Here are the core concepts to understand when using the library:
States as Classes
Each distinct state in your machine is represented by a class that extends the State base class. In these classes, you can override lifecycle hooks and event handlers:
init()– Called when the state is entered. Use this for entry side-effects or initializing state-specific data.processEvent(event)– Called when an event is dispatched to this state. Implement your state's event logic here. Returntrueif the event is handled (to prevent further propagation), orfalseif not handled (so parent or sibling states can process it). This is where you can trigger transitions to other states.destroy()– Called when the state is about to be exited. Use this for cleanup or exit side-effects.
You may define additional methods or properties in your state classes as needed for your application logic. Each state instance can access:
this.fsm– Reference to theFsmcontrolling the state machine (useful if you need to access global FSM context).this.parent– The parent state in a hierarchy (ornullif this state is at the root).this.children– An array of child state instances (for substates/orthogonal regions).
Hierarchical (Nested) States
States can be nested inside other states to form a hierarchy. A parent state can have substates (child states) that are active only when the parent is active. For example, a Moving state might contain Walking and Running substates. When the parent state is entered, one of its child states is initialized immediately (the first child listed is the default). Exiting a parent state will recursively exit its active substates.
You can access ancestor states via the context() helper. For instance, from within a substate you might call this.context(MovingState) to get a reference to the parent MovingState (or null if it’s not in that context).
Orthogonal (Parallel) States
finestate also supports orthogonal regions (parallel states), meaning a state can have multiple independent child state regions active at the same time. This is achieved by providing multiple child state arrays in the state description (see State Configuration below). Each orthogonal region will initialize its own child state, and events can be dispatched to all parallel children. By default, finestate will stop propagating an event to other regions once one child handles it (this behavior can be configured).
Parallel states are useful for representing aspects of state that evolve independently. For example, a game character could have an independent Movement state and Mood state running in parallel. (This is an advanced feature; many use-cases won’t need parallel states.)
Events and Dispatching
An event represents something that causes the state machine to react (e.g. a user action, a timer tick, a message, etc.). finestate allows events to be dispatched as:
- A string or number (for simple event identifiers).
- An object that is an instance of a subclass of the provided
Eventclass (for more complex or typed events).
To send an event into the state machine, call fsm.dispatch(event). The FSM will propagate the event to the active states, following these rules:
- Events are propagated depth-first by default: the most deeply nested active state gets the first chance to handle the event via its
processEvent. If that state doesn’t handle it (processEventreturns false), the event bubbles up to its parent state. - In states with parallel children, the event is forwarded to each child in order. If one child handles the event (returns true), by default the event will not be sent to other parallel children (this is the StopOnProcessed policy). If no child handles it, the parent’s own
processEventis then called. - If a state’s
processEventreturns true (indicating the event was processed and possibly a transition occurred), propagation stops at that state. If it returns false, the event continues up to the next state in the hierarchy. - The return value of
fsm.dispatch(event)will be a boolean indicating if any state in the machine handled the event (trueif handled,falseif unhandled).
Using the event system, your processEvent methods can contain logic to decide transitions. You can compare the event to known types or instances (for example, if (evt instanceof MyCustomEvent) {...} or if (evt === 'someEvent') {...}) and perform actions accordingly.
Event Dispatch Order and Parallel Region Policy (Advanced)
By default, events propagate from child states up to parent states (a depth-first approach). You can customize the dispatch order and how events propagate in parallel regions for advanced scenarios:
Dispatch Order (child-first vs parent-first): Normally, a parent state's children receive events before the parent does (DispatchOrder.Ascending). You can override this by setting the protected dispatchOrder property to DispatchOrder.Descending in a state class (for example, in its constructor or init), causing that parent state to process events before its children. For instance, in the following example the parent logs the event before the child when using descending order:
class Parent extends State {
init() {
// Use parent-first dispatch order:
this.dispatchOrder = DispatchOrder.Descending;
}
...
}or
class Parent extends State {
protected dispatchOrder = DispatchOrder.Descending;
...
}Parallel Regions Policy: If a state has multiple orthogonal child regions, the default policy DispatchOrthoPolicy.StopOnProcessed means that if one region handles an event (its processEvent returns true), the event will not be dispatched to the other regions. To broadcast events to all regions regardless of one handling it, set the state's dispatchOrthoPolicy to DispatchOrthoPolicy.DontStopOnProcessed. In the example below, the first child handles the event, but because we use DontStopOnProcessed, the second child still receives it:
class ParallelParent extends State {
protected dispatchOrthoPolicy = DispatchOrthoPolicy.DontStopOnProcessed;
...
}Custom Dispatch Override: In advanced cases, you can override a state’s dispatch(event) method to intercept or filter events. This is rarely needed, but it allows custom control. For example, a state could log every event and then use the default dispatch behavior:
class LoggingState extends State {
dispatch(event) {
console.log(`LoggingState saw event: ${event}`);
// Perform custom handling or filtering if needed
return super.dispatch(event); // continue with normal dispatch logic
}
}Generally, you will not override dispatch in typical usage, but the option is available if you need to customize event propagation.
Entry and Exit Hooks
As mentioned, each state class can implement init() and destroy() as entry and exit hooks. finestate calls these at the appropriate times:
init()– Called after the state object is constructed and added into the state machine. This is a good place to trigger any actions that should happen upon entering the state (e.g. resetting timers, sending a message, updating UI).destroy()– Called right before the state object is removed from the state machine. Use this to clean up (e.g. clear timers, remove listeners, revert changes made in init).
There is also an initParams(params: StateParams) method you can override if you need to handle initialization data passed via the params object on fsm.init() or transit(). By default, initParams simply calls init(), ignoring any parameters, but you can override it to extract data from params when needed.
These hooks allow each state to manage its own enter/exit side effects in isolation, keeping your state logic encapsulated.
State Configuration
To create an FSM, you need to provide a description of the state structure to the Fsm constructor. finestate uses an array-based configuration to define the hierarchy and parallel regions of states. The formal type definition for a state description is:
// StateType = State class (constructor)
type StateDesc = [ StateType, ...StateDesc[][] ];In simpler terms, a StateDesc is a tuple where the first element is a state class (constructor), and the remaining elements (if any) are arrays that describe that state’s child regions. (In code, you can use the helper function stateDesc(StateClass, ...children) to construct these tuples more readably.)
- The first element of a
StateDescis always aStatesubclass (the state itself). - If a state has no substates, its description is just that state (e.g.
`stateDesc(MyState)`with no child arrays). - If a state has substates, include one array for each orthogonal region:
- For a simple hierarchy (one region of substates), include one array containing the child state descriptions. The first state in that array will be the default initial substate.
- For parallel states, include multiple arrays (as separate regions), one per independent region of substates.
Examples:
`stateDesc(OffState)`– A state with no children.`stateDesc(MovingState, [ stateDesc(WalkingState, [ stateDesc(RunningState) ]) ])`–MovingStatehas one child region. In that region,WalkingStateis listed first (so it’s the default substate), andRunningStateis another possible substate in the same region.`stateDesc(A, [ stateDesc(B) ], [ stateDesc(C) ])`– StateAhas two child regions running in parallel. The first region’s default state isB, and the second region’s default state isC(these would be active concurrently whenAis active).
When you instantiate the Fsm with an array of StateDesc, the first element of the array is taken as the root state of the state machine. For example:
const fsm = new Fsm([
stateDesc(RootState, [
// Child states of RootState (single region)
stateDesc(ChildState1, [
// Child states of ChildState1 (single region)
stateDesc(GrandChildState)
])
])
]);This defines RootState as the top-level state. RootState has one child region containing ChildState1, which in turn has a child region containing GrandChildState. Calling fsm.init() will construct this hierarchy: entering RootState, then ChildState1, then GrandChildState.
You can also define multiple possible root states in the array if your machine can start in different configurations or transition at the top level. In that case, the first state in the list is the initial root, and others are inactive until you transition to them.
Transitions
A transition moves the FSM from one state to another. In finestate, transitions are initiated in code by calling the transit() method, usually inside a state’s processEvent. There are two ways to invoke a transition:
- From within a state: Call
this.transit(TargetStateClass, params?). This will request the FSM to exit the current state (and any necessary parent states) and enter a new state of typeTargetStateClass. The optionalparamsobject can carry data into the new state(s) during initialization. - Directly via the Fsm: You can call
fsm.transit(currentState, TargetStateClass, params?)if you have a reference to a state and want to transition out of it. However, in practice you rarely need to call this directly; using the state’s owntransitmethod is more convenient.
When a transition is triggered, finestate will:
- Call the
destroy()hook on the exiting state and any of its substates that are leaving. - Instantiate the target state (and any of its parent states, if they weren’t already active) and call
init()on them. - If the new state has its own default substates, those will be created and initialized as well.
Transitions can occur between states at any level of the hierarchy. finestate will automatically determine the proper states to exit and the common ancestor where the new state should be inserted. For example, transitioning from a substate to a sibling substate will exit the current substate and possibly its parent, then enter the target state (and re-enter any necessary parent context if needed).
Note: You should not manually instantiate state classes. Always use
transit()to switch states, so that the FSM can manage the lifecycle (calls toinit()/destroy()) correctly.
Note: When calling
this.transit(OtherState)inside aprocessEvent, it’s best toreturnthat call immediately. This ensures the function exits right after initiating the transition (preventing any subsequent logic inprocessEventfrom running) and clearly signals that the event was handled.
Below is an example of an extended hierarchical state machine with parallel regions and a step-by-step demonstration of transitions between states. This scenario illustrates how finestate manages entry (init()) and exit (destroy()) hooks across a nested/parallel configuration.
Transition Sequence Example
Let's suppose we have a state machine with a following diagram:
The declaration will look like this:
class Root extends State {
init() { console.log('Root.init()'); }
destroy() { console.log('Root.destroy()'); }
}
class A1 extends State {
init() { console.log('A1.init()'); }
destroy() { console.log('A1.destroy()'); }
}
...
const fsm = new Fsm([
stateDesc(Root, [
stateDesc(A1, [
// Parallel region #1 within A1
stateDesc(B1),
stateDesc(C1)
], [
// Parallel region #2 within A1
stateDesc(D1),
stateDesc(E1)
]),
stateDesc(A2, [
// Parallel region #1 within A2
stateDesc(B2),
stateDesc(C2)
], [
// Parallel region #2 within A2
stateDesc(D2),
stateDesc(E2)
])
])
]);Below is a hypothetical series of transitions that highlights how init/destroy logs are generated. We assume each transition is triggered via something like this.transit(TargetState) inside a processEvent(...) method.
Initialization (
fsm.init())After calling
init(), the FSM entersRoot, then goes toA1. InsideA1, the first parallel region defaults toB1, and the second toD1.(Active states are highlighted)
Console logs:Root.init() A1.init() B1.init() D1.init()Transition
B1 -> C1
Within the first parallel region of A1,B1exits andC1enters.
Logs:B1.destroy() C1.init()Transition
C1 -> A2
This exit leavesD1andC1(the active regions inside A1), and thenA1itself. We enterA2and its default parallel children (B2andD2).
Logs:D1.destroy() C1.destroy() A1.destroy() A2.init() B2.init() D2.init()Transition
D2 -> E1
SinceE1belongs to A1, we have to exit the entire A2 branch (B2,D2, and A2) and then enter A1. This time, for A1’s first region, we getB1by default again, and in the second region we specifically enterE1.
Logs:D2.destroy() B2.destroy() A2.destroy() A1.init() B1.init() E1.init()Transition
E1 -> C2
Again we exit A1 (thusB1andE1), enter A2, and in the first region we now go directly toC2instead ofB2. The second region defaults toD2.
Logs:E1.destroy() B1.destroy() A1.destroy() A2.init() C2.init() D2.init()Transition
C2 -> A2
If the transition is defined so thatC2leads back to the “same” parent (A2) as a fresh entry, we effectively exitC2,D2, and A2, then re-enter A2 with its default parallel states (B2andD2).
Logs:D2.destroy() C2.destroy() A2.destroy() A2.init() B2.init() D2.init()Transition
B2 -> B2
If the transition is defined to itself, the state will exit and re-enter.
Logs:B2.destroy() B2.init()
Deferred Dispatch and Transition Behavior
finestate prevents reentrant state changes by deferring or forbidding certain calls. If dispatch() or transit() is invoked in the middle of another operation (such as an ongoing dispatch or transition), the library enforces the following rules:
Calling
dispatch()from within anotherdispatch(): If an event is dispatched while a previousdispatchcall is still in progress, the new dispatch call is deferred. The event will not be processed immediately; instead, it is queued to run right after the current dispatch cycle completes. In this scenario, the innerdispatch()returnsfalseimmediately, indicating that the event was not handled in the current cycle since it has been scheduled for processing after the ongoing dispatch completes.Calling
dispatch()from within atransit()orinit(): Ifdispatch()is invoked during a state transition (i.e., from inside atransit()orinit()call), that dispatch is also deferred until the transition is complete. Thedispatch()call returnsfalseright away in this case as well, since the event will only be handled after the current transition ends.Calling
transit()withinfsm.init(): Starting a state transition during the FSM’s initialization phase is not allowed. Iftransit()is called inside anfsm.init()(during the setup of the state machine), finestate will throw an error. This restriction ensures that no transitions occur while the state machine is still establishing its initial state(s).Calling
transit()from within anothertransit(): Nested state transitions are prohibited. If a transition is already in progress andtransit()is invoked again (before the first transition finishes), finestate will throw an error. Only one transition can occur at a time, so any new transition must wait until the current one has fully completed.
API Documentation
Below is an overview of the main classes and types provided by finestate, and their usage. The API is designed to be simple – most interactions are via the Fsm and your own State subclasses.
Fsm class
new Fsm(stateDescriptions: StateDesc[])– Constructs a new state machine using the given state description array. You should provide at least one state description (which will be the root state). Child states and parallel regions are described with nested arrays (or using thestateDeschelper function) as detailed above.init(params?: StateParams): void– Initializes the FSM and enters the initial state configuration. This will construct the root state (and its default child states, recursively) and callinit()on all of them. You can pass an optionalparamsobject that will be forwarded to all states’initParamsfor initial setup data.dispatch(event: EventType): boolean– Dispatches an event into the state machine. The event can be an instance of a subclass ofEvent, a string, or a number. Returnstrueif some state handled the event, orfalseif the event was unhandled. Use this to trigger transitions or actions in response to external inputs.rootState: State | null– A property referencing the current root state object. Afterinit()has been called, this will be the active root state instance. You can use it to inspect the state hierarchy (for example,fsm.rootState!.children[0]to get the first child, etc.). This will change if you transition to a different root state.transit(fromState: State, toStateType: StateType, params?: StateParams): State– Transitions from an existing state to a new state of typetoStateType. This method handles tearing down the appropriate part of the state tree starting atfromStateand constructing the new state in its place. In most cases you don’t need to call this directly (useState.transitinsideprocessEvent).
State class
To create your own states, subclass State. The State class provides methods and properties as the interface for each state:
Lifecycle hooks:
init(): void– Override this to define behavior when the state is entered. This is called after the state is constructed and attached to the FSM.destroy(): void– Override this for behavior when the state is exited and about to be destroyed.initParams(params: StateParams): void– Override it instead ofinit()if you need to handle custom initialization data. By default it callsinit().
Event handling:
processEvent(event: EventType): boolean– Override this to handle incoming events for this state. Returntrueif the event is handled (consumed) by this state (which may include performing a transition or some action). Returnfalseto indicate the event wasn’t handled here, allowing the event to propagate to parent/other states. The default implementation inStatereturnsfalse(no events handled).dispatchChildren(event: EventType): boolean- This method is inherited, and you typically don't override it. The method dispatches the event to children with respect todispatchOrderanddispatchOrthoPolicyproperties of the state. Should returntrueif the event was processed by one of children. Override it if you want to make custom way of the event delivery to the children.dispatch(event: EventType): boolean– This method is inherited, and you typically don’t override it. It will dispatch the event to this state’s children throughdispatchChildren()and then toprocessEventof the current event if the event is not processed by the children. In most cases you will callfsm.dispatch(...)from outside the FSM, rather than callingdispatchon a state directly. You may override the method to make custom events dispatching process.
Transition methods:
transit(targetState: StateType, params?: StateParams): boolean– Call this from within a state (e.g., inprocessEvent) to transition out of the current state to a new state of typetargetState. This is essentially a helper that calls the FSM’stransitmethod for you. It returnstrueto indicate the event triggering the transition was handled.
Context and hierarchy:
fsm: Fsm– Property pointing back to theFsminstance managing this state.parent: State | null– The parent state in the hierarchy (ornullif this state is the root).children: Array<State | null>– An array of this state’s child state instances. The length of this array equals the number of orthogonal (parallel) child regions defined for the state. Each entry is eithernull(if that region has no active state) or a reference to the active child state in that region. For example, a state with one child region will havechildren[0]as its single substate (ornullif uninitialized).context<T extends State>(stateClass: new(...args: any[]) => T): T | null– A helper method to retrieve an ancestor state of a specific class. If this state or any of its parents is an instance ofstateClass, that instance is returned. Otherwise, it returnsnull. This is useful to access shared context or data from a parent state without global variables.
Event dispatch controls (advanced):
dispatchOrder(enumDispatchOrder) – A protected property that determines whether events are sent to child states first or to the state itself first. By default, this isDispatchOrder.Ascending(child states get the event before the parent does, i.e. depth-first propagation). You can set it toDispatchOrder.Descendingin a subclass (e.g., in the constructor orinit) if you need the parent state to process events before children.dispatchOrthoPolicy(enumDispatchOrthoPolicy) – A protected property that controls event propagation in parallel regions. The default isDispatchOrthoPolicy.StopOnProcessed, meaning if one child in an orthogonal set handles the event, other children will not receive it. If you set this toDontStopOnProcessed, every parallel child state will receive the event regardless of others handling it (useful if events should be broadcast to all active substates).
Most of the time, you will only override init, processEvent, and destroy in your states. The other properties and methods are there if you need introspection or advanced control over the state machine’s behavior.
Event class and EventType
finestate provides an abstract Event class which you can extend to create custom event types. This is entirely optional – you can use simple strings or numbers as events if that suits your needs. However, using classes for events can be useful for strong typing and richer data.
class Event– Base class for custom events. (It doesn’t define any properties itself; you can subclass it to add event payload data or simply use the class type to distinguish events.)EventType– This is a union type defined asEvent | string | number. It indicates what can be passed tofsm.dispatchorprocessEvent. So, you can dispatch an instance of your customEventsubclass, or a string/number identifier.
Example: You might define class JumpEvent extends Event { /*...*/ } and then in processEvent(evt), check if (evt instanceof JumpEvent) { ... } to handle that event type.
StateParams
The library defines a StateParams interface (empty by default) that you can extend to pass structured data into state initializations. The params object you provide to fsm.init(params) or transit(..., params) will be received by each new state’s initParams. For example:
interface StateParams {
MyState?: {
level: number;
playerName: string;
}
}
fsm.init({ MyState: { level: 3, playerName: "Alice" } });
...
class MyState extends State {
initParams(params: StateParams) {
const { level, playerName } = params.MyState!;
// Use parameters for the State initialization
}
}Then your state could override initParams (as shown above) to read those values on entry. This is an advanced usage; many times you may not need to pass parameters at initialization.
TypeScript Support and Module Compatibility
finestate is written in TypeScript and ships with complete type definitions. This means you’ll get strong typing and IntelliSense out of the box when using it in a TypeScript project. State classes, events, and the FSM methods all have typed signatures, which helps catch errors at compile time.
Even if you use finestate in a JavaScript project, the type declarations can serve as documentation for the API, and you can refer to them for clarity on how to use the library.
The distributed package includes both ESM (ES6 modules) and CommonJS builds:
- For modern bundlers or Node.js (ESM), the
moduleentry point is used (tree-shaking friendly). - For older environments or CommonJS usage, the
mainentry point provides a CJS bundle.
This means you can import finestate in whatever way your project requires, and it should work seamlessly. The library has no external runtime dependencies, so it’s straightforward to include in browsers or Node. It’s also framework-agnostic – use it with React, Angular, or plain Node.js scripts as needed.
License
finestate is open source and licensed under the MIT License. You are free to use it in commercial or personal projects. Please see the LICENSE file for the full text of the license.
Contributing
Contributions are welcome! If you find a bug or have an idea for an improvement, feel free to open an issue or submit a pull request on GitHub. When contributing code, please ensure that all tests pass (npm test) and add new tests for any new functionality.
We appreciate the community’s help in making finestate better. Whether it’s reporting issues, writing documentation, or adding features, any contribution is valuable.
Happy state modeling with finestate!
