Skip to content

FsmConfig

Defined in: types.ts:615

Configuration object for creating an FSM.

// TCtx inferred as { tickCount: number }, TStates inferred from states object:
createFsm({
  id: "traffic-light",
  initialState: "green",         // validated against state keys
  context: { tickCount: 0 },     // inference site for TCtx
  states: {
    green:  { timeout: "yellow" }, // "yellow" validated against state keys
    yellow: { timeout: "red" },
    red:    { timeout: "green" },
  },
});
// This FSM fires "phaseComplete" at itself but never handles it — it
// expects whatever mounts it via `_child` to catch the bubble.
const phaseController = createFsm({
  id: "phase-controller",
  initialState: "green",
  bubbles: ["phaseComplete"],
  states: {
    green: { advance: "red" },
    red: {
      _onEnter({ ctx }) {
        setTimeout(() => phaseController.handle("phaseComplete"), 0);
      },
    },
  },
});

// Mounting it without handling "phaseComplete" (and without re-declaring
// it in this FSM's own `bubbles`) is a compile error on `_child` below.
const intersection = createFsm({
  id: "intersection",
  initialState: "northSouth",
  states: {
    northSouth: {
      _child: phaseController,
      phaseComplete: "clearance", // this line is what covers the bubble
    },
    clearance: { advance: "northSouth" },
  },
});

TCtx

The context type (Fsm) or client type (BehavioralFsm). For Fsm, this is inferred from the context property. For BehavioralFsm, it’s the client object type provided explicitly or as a generic parameter.

TStates extends Record<string, Record<string, unknown>> = Record<string, Record<string, unknown>>

The literal states object type. Captured directly from the naked states: TStates & ... intersection member below (ideally with const on the factory’s generic to preserve string literal types) — deliberately NOT derived from ValidateStates, so an empty or function-only state can’t disable inference (see ValidateStates’ module comment for the full mechanism). Defaults to a loose record for unconstrained usage.

TStateNames extends string = keyof TStates & string

The state-name union, defaulted from TStates but captured as its OWN parameter (see ValidateStates) so it’s available, fully resolved, while handler bodies are still being type-checked.

TBubbles extends string = never

The union of inputs this FSM declares via bubbles. Defaults to never — most FSMs bubble nothing.

optional bubbles: readonly TBubbles[]

Defined in: types.ts:666

Inputs this FSM fires at itself without handling them — expecting whatever mounts it via _child to catch them through machina’s nohandler-bubbling mechanism (see the “Input delegation” section of the hierarchical states guide). Declaring a bubble does two things:

  1. Joins this FSM’s own typed input union, so a self-directed fsm.handle("phaseComplete") (e.g. from inside _onEnter) type-checks without a cast.
  2. Becomes part of this FSM’s mounting contract: any config that mounts it via _child must handle every declared bubble in some state, re-declare it in its OWN bubbles (passing the obligation up another level), or carry a "*" catch-all — enforced at compile time by ChildCoverage, which is intersected onto states below.

A bubble name is NOT a state name — it can’t be used as a string shorthand transition target. Omit bubbles entirely (the default) for an FSM that never expects a container to catch anything from it — such an FSM can be mounted via _child anywhere with no obligations.


optional context: TCtx

Defined in: types.ts:644

Initial context data. The type is inferred from this value and flows into every handler’s ctx parameter.

For BehavioralFsm, this property is optional and serves only as a type constraint — the client object IS the context.


id: string

Defined in: types.ts:625

Unique identifier for this FSM


initialState: NoInfer<TStateNames>

Defined in: types.ts:635

The state to start in. Must be a key of states.

Wrapped in NoInfer to prevent TypeScript from using this value as an inference site for TStateNames. Without it, initialState: "green" could narrow the state-name union to only have a “green” key. We want inference to come exclusively from the states property.


states: TStates & ValidateStates<TCtx, TStates, TStateNames> & ChildCoverage<TStates, TStateNames, TBubbles>

Defined in: types.ts:677

State definitions. Keys become the state name union.

The intersection with the bare TStates is what makes literal-type capture work for empty and function-only states (see ValidateStates’ module comment) — ValidateStates and ChildCoverage layer validation and the bubble-coverage contract on top without becoming the inference source themselves.