Skip to content

createFsm

createFsm<TInputs>(): <TCtx, TStates, TStateNames, TBubbles>(config) => Fsm<TCtx, keyof TStates & string, keyof TInputs & string, TBubbles, TInputs>

Defined in: fsm.ts:292

Create a single-client FSM from a config object.

Generic parameters are inferred automatically:

  • TCtx comes from config.context (defaults to {} if omitted).
  • TStates is captured with const inference to preserve string literal types, enabling compile-time validation of transition targets and handle() input names.

State names, input names, and all handler signatures derive from TStates.

To type each input’s payload as well (#195), call the curried form with an input payload map: createFsm<TInputs>()({...}). The zero-argument call fixes TInputs; the returned function infers everything else from the config exactly as the direct form does. The split exists because TypeScript has no partial type-argument inference — the map can’t ride along with the inferred parameters. Under a map, handler payload parameters are inferred from the map’s tuples and handle() enforces them at the call site. The map is also the complete input vocabulary: handler keys outside it are compile errors.

TInputs extends InputMapLike<TInputs>

<TCtx, TStates, TStateNames, TBubbles>(config) => Fsm<TCtx, keyof TStates & string, keyof TInputs & string, TBubbles, TInputs>

const light = createFsm({
  id: "traffic-light",
  initialState: "green",
  context: { tickCount: 0 },
  states: {
    green:  { timeout: "yellow" },
    yellow: { timeout: "red" },
    red:    { timeout: "green" },
  },
});

light.handle("timeout"); // transitions green → yellow

Typed input payloads via the curried form

type TrafficLightInputs = {
  timeout: [];
  emergency: [event: { severity: number }];
};

const light = createFsm<TrafficLightInputs>()({
  id: "traffic-light",
  initialState: "green",
  context: { minSeverity: 3 },
  states: {
    green: {
      timeout: "yellow",
      // `event` is inferred as { severity: number }
      emergency({ ctx }, event) {
        if (event.severity < ctx.minSeverity) { return; }
        return "red";
      },
    },
    yellow: { timeout: "red" },
    red: {},
  },
});

light.handle("emergency", { severity: 5 }); // payload enforced

createFsm<TCtx, TStates, TStateNames, TBubbles>(config): Fsm<TCtx, keyof TStates & string, TBubbles | Exclude<{ [S in string | number | symbol]: keyof TStates[S] & string }[keyof TStates], SpecialStateKeys> | { [S in string | number | symbol]: TStates[S] extends { _child: C } ? InputNamesOfInstance<C> : never }[keyof TStates], TBubbles>

Defined in: fsm.ts:317

Create a single-client FSM from a config object.

Generic parameters are inferred automatically:

  • TCtx comes from config.context (defaults to {} if omitted).
  • TStates is captured with const inference to preserve string literal types, enabling compile-time validation of transition targets and handle() input names.

State names, input names, and all handler signatures derive from TStates.

To type each input’s payload as well (#195), call the curried form with an input payload map: createFsm<TInputs>()({...}). The zero-argument call fixes TInputs; the returned function infers everything else from the config exactly as the direct form does. The split exists because TypeScript has no partial type-argument inference — the map can’t ride along with the inferred parameters. Under a map, handler payload parameters are inferred from the map’s tuples and handle() enforces them at the call site. The map is also the complete input vocabulary: handler keys outside it are compile errors.

TCtx extends object = Record<string, never>

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

TStateNames extends string = keyof TStates & string

TBubbles extends string = never

FsmConfig<TCtx, TStates, TStateNames, TBubbles>

Fsm<TCtx, keyof TStates & string, TBubbles | Exclude<{ [S in string | number | symbol]: keyof TStates[S] & string }[keyof TStates], SpecialStateKeys> | { [S in string | number | symbol]: TStates[S] extends { _child: C } ? InputNamesOfInstance<C> : never }[keyof TStates], TBubbles>

const light = createFsm({
  id: "traffic-light",
  initialState: "green",
  context: { tickCount: 0 },
  states: {
    green:  { timeout: "yellow" },
    yellow: { timeout: "red" },
    red:    { timeout: "green" },
  },
});

light.handle("timeout"); // transitions green → yellow

Typed input payloads via the curried form

type TrafficLightInputs = {
  timeout: [];
  emergency: [event: { severity: number }];
};

const light = createFsm<TrafficLightInputs>()({
  id: "traffic-light",
  initialState: "green",
  context: { minSeverity: 3 },
  states: {
    green: {
      timeout: "yellow",
      // `event` is inferred as { severity: number }
      emergency({ ctx }, event) {
        if (event.severity < ctx.minSeverity) { return; }
        return "red";
      },
    },
    yellow: { timeout: "red" },
    red: {},
  },
});

light.handle("emergency", { severity: 5 }); // payload enforced