Skip to content

createBehavioralFsm

createBehavioralFsm<TClient>(): <TStates, TStateNames, TBubbles>(config) => BehavioralFsm<TClient, 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: behavioral-fsm.ts:1185

Create a behavioral FSM (one definition, many clients) from a config object.

TClient can’t be inferred from the config — there’s no context property at the FSM level for it to hook into, unlike createFsm’s TCtx. Supplying it as a single explicit type argument (createBehavioralFsm<Connection>({...})) doesn’t work either: TypeScript has no partial type-argument inference, so providing one of two type parameters disables inference for the other, widening TStates to Record<string, Record<string, unknown>> and discarding all literal-type validation.

The fix is currying: call with zero arguments to fix TClient, then call the returned function with the config to infer TStates from it, const literal capture and all. This is the recommended way to type a client.

If you’d rather skip the type argument entirely, annotate ctx inline on at least one handler (e.g. disconnect({ ctx }: { ctx: Connection }) {...}) and call with zero type arguments — TClient is then inferred from that annotation. This only works when the annotation is visible directly on a handler; it does NOT work through object spread (...guards), since TypeScript won’t look inside a spread for the annotation.

TClient extends object

<TStates, TStateNames, TBubbles>(config): BehavioralFsm<TClient, 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>

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

TStateNames extends string = keyof TStates & string

TBubbles extends string = never

FsmConfig<TClient, TStates, TStateNames, TBubbles>

BehavioralFsm<TClient, 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>

interface Connection { url: string; retries: number; }

const connFsm = createBehavioralFsm<Connection>()({
  id: "connectivity",
  initialState: "disconnected",
  states: {
    disconnected: { connect: "connecting" },
    connecting:   { connected: "online", failed: "disconnected" },
    online:       { disconnect: "disconnected" },
  },
});

const conn = { url: "wss://example.com", retries: 0 };
connFsm.handle(conn, "connect");
const connFsm = createBehavioralFsm({
  id: "connectivity",
  initialState: "disconnected",
  states: {
    disconnected: {
      connect({ ctx }: { ctx: Connection }) { return "connecting"; },
    },
    connecting: { connected: "online", failed: "disconnected" },
    online:     { disconnect: "disconnected" },
  },
});
// childFsm fires "phaseComplete" at itself but never handles it — it
// expects whatever mounts it via `_child` to catch the bubble.
const childFsm = createBehavioralFsm<Connection>()({
  id: "child",
  initialState: "green",
  bubbles: ["phaseComplete"],
  states: {
    green: { advance: "red" },
    red: {},
  },
});

// Mounting it without handling (or re-declaring) "phaseComplete" is a
// compile error on `_child` below.
const parentFsm = createBehavioralFsm<Connection>()({
  id: "parent",
  initialState: "active",
  states: {
    active: {
      _child: childFsm,
      phaseComplete: "cooldown", // covers the bubble
    },
    cooldown: { advance: "active" },
  },
});

createBehavioralFsm<TClient, TStates, TStateNames, TBubbles>(config): BehavioralFsm<TClient, 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: behavioral-fsm.ts:1207

Create a behavioral FSM (one definition, many clients) from a config object.

TClient can’t be inferred from the config — there’s no context property at the FSM level for it to hook into, unlike createFsm’s TCtx. Supplying it as a single explicit type argument (createBehavioralFsm<Connection>({...})) doesn’t work either: TypeScript has no partial type-argument inference, so providing one of two type parameters disables inference for the other, widening TStates to Record<string, Record<string, unknown>> and discarding all literal-type validation.

The fix is currying: call with zero arguments to fix TClient, then call the returned function with the config to infer TStates from it, const literal capture and all. This is the recommended way to type a client.

If you’d rather skip the type argument entirely, annotate ctx inline on at least one handler (e.g. disconnect({ ctx }: { ctx: Connection }) {...}) and call with zero type arguments — TClient is then inferred from that annotation. This only works when the annotation is visible directly on a handler; it does NOT work through object spread (...guards), since TypeScript won’t look inside a spread for the annotation.

TClient extends object

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

TStateNames extends string = keyof TStates & string

TBubbles extends string = never

FsmConfig<TClient, TStates, TStateNames, TBubbles>

BehavioralFsm<TClient, 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>

interface Connection { url: string; retries: number; }

const connFsm = createBehavioralFsm<Connection>()({
  id: "connectivity",
  initialState: "disconnected",
  states: {
    disconnected: { connect: "connecting" },
    connecting:   { connected: "online", failed: "disconnected" },
    online:       { disconnect: "disconnected" },
  },
});

const conn = { url: "wss://example.com", retries: 0 };
connFsm.handle(conn, "connect");
const connFsm = createBehavioralFsm({
  id: "connectivity",
  initialState: "disconnected",
  states: {
    disconnected: {
      connect({ ctx }: { ctx: Connection }) { return "connecting"; },
    },
    connecting: { connected: "online", failed: "disconnected" },
    online:     { disconnect: "disconnected" },
  },
});
// childFsm fires "phaseComplete" at itself but never handles it — it
// expects whatever mounts it via `_child` to catch the bubble.
const childFsm = createBehavioralFsm<Connection>()({
  id: "child",
  initialState: "green",
  bubbles: ["phaseComplete"],
  states: {
    green: { advance: "red" },
    red: {},
  },
});

// Mounting it without handling (or re-declaring) "phaseComplete" is a
// compile error on `_child` below.
const parentFsm = createBehavioralFsm<Connection>()({
  id: "parent",
  initialState: "active",
  states: {
    active: {
      _child: childFsm,
      phaseComplete: "cooldown", // covers the bubble
    },
    cooldown: { advance: "active" },
  },
});