This library provides a set of lightweight utilities for working with tagged unions and state machines. These helpers are available in both the main library and the @doeixd/machine/minimal module.
The tag function is a helper for creating tagged objects. It ensures that the tag property is correctly treated as a literal type by TypeScript, which is essential for pattern matching and state narrowing.
import { tag } from "@doeixd/machine";
const state = tag('idle');
// { tag: 'idle' }const state = tag('loading', { url: '/api' });
// { tag: 'loading', url: '/api' }You can also use it to ensure an existing object is treated as a literal tag.
const state = tag({ tag: 'success', data: 'ok' });Creates a reusable factory for a specific tagged state. This is highly ergonomic when you need to create the same state multiple times, particularly inside machine transitions.
import { tag, type States } from "@doeixd/machine";
// 1. Define your states
type AppState = States<{
idle: { count: number };
active: { count: number; user: string };
}>;
// 2. Create factories (using the curried version for unions)
const idle = tag.factory<AppState>()('idle');
const active = tag.factory<AppState>()('active');
// 3. Use in transitions
const nextState = idle({ count: 10 });
// { tag: 'idle', count: 10 }tag.enum() creates a frozen, read-only namespace of named tag factories. It
has an inferred form for open payloads and a generic form for a fixed tagged
union schema.
Pass definitions directly when the tags are fixed but each call should infer its own payload:
import { tag } from '@doeixd/machine';
const Status = tag.enum(
tag('idle'),
tag('loading'),
tag('success'),
);
const idle = Status.idle();
// { tag: 'idle' }
const loading = Status.loading({ url: '/api' });
// { tag: 'loading', url: '/api' }
// Status.missing(); // TypeScript errorPayloads are inferred independently for each call. tag.enum() defines the available names, not a fixed payload schema. Use States<...> with union() when each tag must always carry a specific payload:
const first = Status.loading({ url: '/api' });
const second = Status.loading({ attempt: 2 });
// Both are valid: the inferred form does not prescribe loading's payload.Supply a tagged union as the generic argument when each name has a durable payload contract. Because TypeScript generic arguments cannot be inferred from later function arguments, this form has one additional call:
type RequestState = States<{
idle: {};
loading: { url: string };
success: { data: string };
}>;
const Request = tag.enum<RequestState>()(
tag('idle'),
tag('loading'),
tag('success'),
);
Request.idle();
Request.loading({ url: '/api' });
Request.success({ data: 'done' });
// Request.loading(); // TypeScript error: url is required
// Request.loading({ url: 42 }); // TypeScript error: url must be a string
// tag.enum<RequestState>()(tag('x')); // TypeScript error: unknown tagAn empty payload ({}) produces a zero-argument factory. A non-empty payload is
required and checked with ordinary TypeScript structural typing. The result
only exposes definitions you supplied, so you may intentionally create a subset
of a larger union:
const Pending = tag.enum<RequestState>()(
tag('idle'),
tag('loading'),
);
// Pending.success does not exist.Both forms reject duplicate definitions such as
tag.enum(tag('idle'), tag('idle')) at construction instead of silently
replacing a factory. Payload objects cannot override the generated tag field.
- Use
tag.enum(tag(...), ...)for a convenient namespace whose payload shape is intentionally chosen at each call. - Use
tag.enum<State>()(tag(...), ...)for state machines and domain models where each tag has one stable payload schema. This is the safer default withStates<...>andunion().
A type guard for checking the current state of a machine or tagged object. It narrows the type within the conditional block.
import { isState } from "@doeixd/machine";
if (isState(machine, 'loading')) {
// TypeScript knows machine is { tag: 'loading', url: string }
console.log(machine.url);
}Recursively freezes an object and all of its properties. Useful for ensuring immutability of context or machines.
import { freeze } from "@doeixd/machine";
const ctx = freeze({
user: { id: 1, name: 'Alice' }
});
// ctx.user.name = 'Bob'; // ❌ Throws error in strict modeExtracts the context type from a machine. Works with both regular Machine and minimal Machine.
import { type Context } from "@doeixd/machine";
type MyContext = Context<typeof myMachine>;Extracts the transition signatures from a machine.
import { type Transitions } from "@doeixd/machine";
type MyTransitions = Transitions<typeof myMachine>;Extracts the return type from a machine factory.
import { type MachineOf } from "@doeixd/machine";
type MyMachine = MachineOf<typeof createMyMachine>;Extracts the union of all possible states from a union factory.
import { union, type UnionOf } from "@doeixd/machine/minimal";
const auth = union<State>()({ ... });
type AuthMachine = UnionOf<typeof auth>;A powerful utility to define a tagged union from a mapping of tags to data objects. This is much more ergonomic than writing unions manually.
import { type States } from "@doeixd/machine";
type PickMode = States<{
idle: {},
active: { isCloseMode: boolean; timeoutId: number }
}>;
// Explicit union for comparison:
// type PickMode =
// | { readonly tag: "idle" }
// | { readonly tag: "active"; isCloseMode: boolean; timeoutId: number };