API Reference
API Reference
Complete API documentation for StateFlow functions, types, and patterns.
Core Functions
StateFlow's essential functions for state management:
About the examples
The examples below are illustrative fragments. Identifiers such as taskSignals, taskState, uiState, userState, and signals are assumed to be defined elsewhere (e.g. via defineSignal/defineState) and are not fully scaffolded here.
defineState
Creates a state definition with typed variants using a builder pattern.
function defineState<TProps>(): StateBuilder<TProps, "", unknown, "">
Builder methods:
.name(string)- Set unique state name.variant(name, isInitial?)- Add state variant.signals(object)- Define handled signals.parser(func)- Custom property parsing.stringRepr(func)- Custom string representation.build()- Finalize definition
Example:
const taskState = defineState<{
id: string;
title: string;
assignee?: string;
priority: 'low' | 'medium' | 'high';
}>()
.name("task")
.signals(taskSignals)
.variant("draft", true)
.variant("assigned")
.variant("completed")
.stringRepr(s => `${s.title} (${s.priority})`)
// `.parser` receives `object`, so cast before reading properties.
.parser((obj) => {
const o = obj as Partial<{
id: string;
title: string;
assignee?: string;
priority: 'low' | 'medium' | 'high';
}>;
return {
id: o.id || crypto.randomUUID(),
title: o.title || "Untitled",
assignee: o.assignee,
priority: o.priority || 'medium'
};
})
.build();
defineSignal
Creates a signal definition for triggering state transitions.
function defineSignal<TArgs extends Record<string, unknown> | void = void>(
name: string,
stringRepr?: (args: TArgs) => string
): SignalDefinition<TArgs>
Parameters:
name- Unique signal identifierstringRepr- Optional custom string representation
Returns signal factory function.
Example:
// Parameterless signal
const refresh = defineSignal("refresh");
const signal1 = refresh(); // Creates signal instance
// Parameterized signal
const updatePriority = defineSignal<{
taskId: string;
priority: 'low' | 'medium' | 'high';
}>("updatePriority");
const signal2 = updatePriority({
taskId: "123",
priority: "high"
});
// Custom string representation
const complexSignal = defineSignal<{
action: string;
metadata: Record<string, unknown>;
}>("complexSignal", (args) => `${args.action}:${Object.keys(args.metadata).length} props`);
defineFlow
Defines signal handlers for a state variant.
function defineFlow(state: StateVariant, handlers: SignalHandlers): void
Handler signature: (state, signal, context) => StateResult
Example:
defineFlow(taskState.draft, {
assign: (state, signal) => {
if (!signal.userId) {
return Result.reject("User ID required for assignment");
}
return taskState.assigned({
...state,
assignee: signal.userId
});
},
updatePriority: (state, signal) => ({
...state,
priority: signal.priority
}),
delete: () => Result.reject("Cannot delete draft tasks")
});
applyFlow
Applies state definitions to an application object and sets up handlers.
function applyFlow(target: object, states: StateDefinition[],
initializer: (sm: StateManager) => void,
config?: FlowConfig): void
StateManager methods:
addEnterHandler(state, handler)- Called when entering stateaddExitHandler(state, handler)- Called when leaving stateaddUpdateHandler(state, handler)- Called when state data changesaddRollbackHandler(state, handler)- Called on transition failures
Example:
const app = {
task: { id: "", title: "", priority: "medium" as const },
ui: { loading: false, error: null }
};
applyFlow(app, [taskState, uiState], (sm) => {
sm.addEnterHandler(taskState.assigned, async (state) => {
return Result.transition(async () => {
await notifyUser(state.assignee, state);
return Result.ok();
}, 3000);
});
sm.addExitHandler(uiState.error, (state) => {
clearErrorDisplay();
return Result.ok();
});
});
dispatch
Dispatches a signal to trigger state transitions.
function dispatch(target: object, signal: Signal, mute?: boolean): Result
Deprecated for general use. dispatch() throws if a lock is held or a transition is in flight, so prefer lock() + send() everywhere (see below). Reserve bare dispatch() only for synchronous teardown (e.g. beforeunload) and pre-lock bootstrap, where an async lock cannot be acquired. Returns a Result; on an async (InTransition) result, .expect() is enforced at .done(), so an async dispatch must chain .done().
Example:
// Basic dispatch with expectation
try {
await dispatch(app, signals.assign({ userId: "user123" }))
.expect(ResultKind.OK)
.done();
console.log("Task assigned successfully");
} catch (error) {
console.error(`Assignment failed: ${error}`);
}
// Asynchronous dispatch
await dispatch(app, signals.save()).done();
console.log('Save completed');
// Muted dispatch (no logging)
await dispatch(app, signals.ping(), true).done();
// Multiple expected results
await dispatch(app, signals.optionalAction())
.expect(ResultKind.OK, ResultKind.Ignored)
.done();
observe
Watches for changes in specific state variants.
function observe(target: object, stateVariants: StateVariant[],
handler: (state: StateInstance) => void,
compareFn?: (prev, curr) => boolean): Disposer
Returns disposable observer. Use [Symbol.dispose]() for cleanup.
Example:
// Basic observation
const observer = observe(
app,
[taskState.assigned, taskState.completed],
(state) => updateTaskDisplay(state)
);
// With custom comparison
const priorityObserver = observe(
app,
[taskState.assigned],
(state) => highlightHighPriority(state),
(prev, curr) => prev.priority !== curr.priority
);
// Using the using keyword (Explicit Resource Management (`using`) —
// requires TypeScript 5.2+ and the ESNext.Disposable lib)
function watchTasks() {
using observer = observe(app, [taskState.assigned], updateUI);
// Observer automatically disposed when scope exits
}
// Manual disposal
const sub = observe(app, [taskState.completed], logCompletion);
// Later...
sub[Symbol.dispose]();
lock
Acquires an exclusive lock on the target for dispatching multiple signals in sequence. Uses await using for automatic cleanup.
async function lock(target: object, label?: string): Promise<DispatchFn>
Returns a callable DispatchFn that dispatches signals while holding the lock. The function also implements Symbol.asyncDispose for automatic release.
- Acquires the lock first, then awaits
sync(target)to drain any in-flight transitions - If another lock is held, waits in queue until released
dispatch()throws if called while a lock is held — uselock()instead
Example:
// Multiple dispatches in one critical section
await using send = await lock(app);
await send(signals.activate()).done();
await send(signals.configure({ setting: "value" })).done();
// lock released automatically at scope exit
// Locks queue — second caller waits for the first
async function operationA() {
await using send = await lock(app);
send(signals.step1());
send(signals.step2());
}
async function operationB() {
await using send = await lock(app); // waits for operationA's lock
send(signals.step3());
}
sync
Waits for all pending async transitions to complete.
async function sync(target: object): Promise<void>
Essential before operations requiring stable state.
Example:
async function performComplexOperation() {
// Ensure no transitions are pending
await sync(app);
// Use lock for multiple dispatches
await using send = await lock(app);
await send(signals.startBatch()).done();
await send(signals.process({ id: "1" })).done();
await send(signals.finalizeBatch()).done();
}
Result API
Results provide explicit feedback for all operations with type-safe handling.
Result Types
enum ResultKind {
OK, Ignored, InTransition, Rejected, Error
}
class Result<TData = unknown> {
readonly kind: ResultKind;
readonly data: TData | null;
get error(): Error | null;
get message(): string | null;
in(...kinds: ResultKind[]): boolean;
done(): Promise<Result>;
expect(...kinds: ResultKind[]): this;
}
Static Methods
Result.ok(data?) // Success
Result.ignore(message) // Signal not applicable
Result.reject(message) // Validation failed
Result.error(error) // Exception occurred
Result.transition(asyncFn, timeout?) // Async operation
Result.enqueue(signal) // Success + chain follow-up signal
Result.enqueue
Creates an OK result that chains a follow-up signal for dispatch after the current dispatch completes successfully. The entire chain is atomic — if the enqueued signal fails, all state changes (including the original dispatch) are rolled back.
Result.enqueue(signal: StateSignal): Result
Example:
// In state handlers — enqueue a follow-up signal
sm.addEnterHandler(driverState.active, (state) => {
return Result.enqueue(signals.updateDriver({ driver: state.driver }));
});
// Calling dispatch(app, signals.activateDriver()) will:
// 1. Execute activateDriver -> enters active state
// 2. Automatically dispatch updateDriver within the same lock
// 3. Return the final result only after both complete
// 4. If updateDriver fails, roll back to state before activateDriver
// A self-terminating same-target record chain is supported: an update enqueues a record
// signal whose same-variant transition re-runs the update, which re-verifies and converges.
sm.addUpdateHandler(driverState.active, (state) => {
return state.recorded ? Result.ok() : Result.enqueue(signals.record());
});
Rules: at most one Result.enqueue per handler, and don't let two different handlers each enqueue in the same dispatch cycle (the engine dev-warns it). The supported "chain" is the self-terminating same-target record pattern above, which converges to a fixed point — not a way to script arbitrary multi-step workflows. For those, and for any cross-target follow-up, use lock() + send() on the target.
Other Static Methods
// In flow handlers
defineFlow(orderState.pending, {
confirm: (state) => {
if (!state.paymentVerified) {
return Result.reject("Payment not verified");
}
if (state.items.length === 0) {
return Result.ignore("No items in order");
}
try {
validateOrder(state);
return orderState.confirmed(state);
} catch (error) {
return Result.error(error);
}
}
});
// In state handlers
sm.addEnterHandler(orderState.processing, (state) => {
return Result.transition(async () => {
try {
await processPayment(state);
await updateInventory(state);
return Result.ok();
} catch (error) {
return Result.error(error);
}
}, 10000); // 10 second timeout
});
Type Utilities
TypeScript utilities for extracting type information:
Infer
Extracts property types from states, variants, or signals.
type UserProps = Infer<typeof userState>; // State properties
type ActiveProps = Infer<typeof userState.active>; // Variant properties
type SignalArgs = Infer<typeof updateSignal>; // Signal arguments
Infer is the only type utility exported from @state-flow/core. The following are internal helpers (not re-exported from the package entry point):
ExtractVariants<T>- Get variant names (internal, not exported)ExtractSignals<T>- Get signal types (internal, not exported)ExtractName<T>- Get state name (internal, not exported)ArrayToRecord<T>- Convert state array to record (internal, not exported)
Error Types
StateFlow uses custom errors for framework issues:
class StateFlowError extends Error {
name: "StateFlowError";
}
Common errors:
- Redefining flows for same state variant
- Dispatching signals during active transitions
- Invalid state configuration (missing name/variants)
The StateFlow API provides type-safe, comprehensive feedback for predictable state management.