Setup
machine() builds and runs a machine. setup is how you author the config with full type-checking, via two paths off one entry: setup.infer() (types inferred from the literal) and setup.as<Ctx, Ev>() (you pin the types, names become compile-checked).
machine(config)
Section titled “machine(config)”import { machine, act } from '@dunky.dev/state-machine'
const toggle = machine({ initial: 'inactive', context: { count: 0 }, states: { inactive: { on: { flip: { target: 'active', actions: act($ => ({ count: $.context.count + 1 })) }, }, }, active: { on: { flip: { target: 'inactive' } }, }, },})
toggle.start()toggle.send({ type: 'flip' })toggle.state // 'active'toggle.context // { count: 1 }Returns a service, built but not running. Call .start() to boot effects and watchers, .stop() to tear them down. .send() dispatches events; .state and .context are always readable.
setup.infer(), inferred path
Section titled “setup.infer(), inferred path”Infers State, Context, and Event straight from the literal: no type arguments, no manual annotations. Named guards / actions / effects / delays stay as loose strings.
import { setup } from '@dunky.dev/state-machine'
const config = setup.infer().createMachine({ initial: 'closed', context: { open: false }, states: { closed: { on: { toggle: { target: 'open' } } }, open: { on: { toggle: { target: 'closed' } } }, },})Everything below was inferred from that object. You wrote no types:
// State inferred as 'closed' | 'open' (from the `states` keys)// Context inferred as { open: boolean } (from `context`)// Event inferred as { type: 'toggle' } (from the `on` keys)
const m = machine(config)m.matches('open') // ✅ autocompletes 'closed' | 'open'm.matches('half') // ✗ type error: not a known statem.send({ type: 'tooggle' }) // ✗ type error: not a known eventInside the config, act callbacks get the same inference: $.context.open is boolean, and the event in a handler is narrowed to the one that triggered it. Nothing here was declared; it all flows from the literal.
setup.as<Ctx, Ev>(), checked path
Section titled “setup.as<Ctx, Ev>(), checked path”Pin Context and Event explicitly, then name a registry of guards / actions / effects / delays. Every reference in the config is compile-checked and autocompleted against the registry keys; a typo is a type error.
import { setup } from '@dunky.dev/state-machine'
type Ctx = { open: boolean; locked: boolean }type Ev = { type: 'toggle' }
const { createMachine } = setup.as<Ctx, Ev>().config({ guards: { isUnlocked: ({ context }) => !context.locked, }, actions: { logOpen: ({ context }) => console.log('open:', context.open), },})
const config = createMachine({ initial: 'closed', context: { open: false, locked: false }, states: { closed: { on: { toggle: { target: 'open', guard: 'isUnlocked', actions: 'logOpen' }, }, }, open: { on: { toggle: { target: 'closed' } }, }, },})Which one?
Section titled “Which one?”The two paths are identical at runtime — same builder underneath. The difference is entirely type-level, and comes down to one question: do you want guard / action / effect / delay names compile-checked?
setup.infer()— lightweight path. Config is a self-contained literal (inline functions, or you’re fine with loose string names). You getState/Context/Eventinference for free with zero annotations. Good for small machines and prototyping.setup.as<Ctx, Ev>()— checked path. You maintain a named registry of guards / actions / effects / delays and want every reference in the config verified and autocompleted. The cost is pinningCtx/Evyourself and adding the.config()step; the payoff is that a typo or a deleted implementation becomes a compile error instead of a runtime surprise.
What .infer() won’t catch
Section titled “What .infer() won’t catch”Under .infer(), named references stay loose strings (typed as string, not a union of your registry keys). Every line below compiles clean and only misbehaves at runtime — a silent no-op:
// setup.infer() — none of these are type errors:actions: 'logOpn' // typo → compiles, silently does nothingguard: 'isUnlockd' // typo → compileseffects: 'trak' // typo → compilesafter: { opnDelay: { }} // typo → compilesThe same misspellings under setup.as<Ctx, Ev>().config(...) are compile errors, because .config() narrows the name unions to the registry’s actual keys. You also lose autocomplete for those names on the .infer() path — the editor has no union to suggest from.