TypeScript Inference Tips
To ensure the best developer experience with Matchina:
- Define your transitions inside your
createMachinefunction call - Avoid standalone transition objects: These break TypeScript’s ability to validate your code
- Let TypeScript help you: Embrace the rich type checking and autocomplete features
Recommended Patterns
Section titled “Recommended Patterns”Inline Transitions
Section titled “Inline Transitions”For full type inference to work properly, these transitions should be defined inside createMachine() rather than as standalone objects.
Benefits of this approach:
- Full type checking between states and transitions
- Autocomplete for state names in transitions
- Parameter type checking for transition functions
- Prevents common errors like typos in state names
Use satisfies for Standalone Transitions
Section titled “Use satisfies for Standalone Transitions”When defining transitions as standalone variables, use the satisfies FactoryMachineTransitions<typeof states> to ensure they match the expected type. This provides additional type safety and helps catch errors early.
Comparing Practices
Section titled “Comparing Practices”Compare the type support for inline transitions versus standalone transition objects:
import { function defineStates<Config extends TaggedTypes>(config: Config): StateMatchboxFactory<Config>
defineStates, function matchina<SF extends KeyedStateFactory, TC extends FactoryMachineTransitions<SF>, FC extends FactoryMachineContext<SF> = { states: SF; transitions: TC;}>(states: SF, transitions: TC, init: KeysWithZeroRequiredArgs<FC["states"]> | FactoryKeyedState<FC["states"]>): import("/home/runner/work/matchina/matchina/dist/factory-machine-types").FactoryMachine<FC> & (FC extends FactoryMachineContext<any> ? import("/home/runner/work/matchina/matchina/dist/utility-types").DrainOuterGeneric<(object & import("/home/runner/work/matchina/matchina/dist/utility-types").TUnionToIntersection<import("/home/runner/work/matchina/matchina/dist/utility-types").FlatMemberUnion<import("/home/runner/work/matchina/matchina/dist/factory-machine-api-types").StateEventTransitionSenders<FC, keyof FC["transitions"]>>> extends infer T ? { [K in keyof T]: T[K]; } : never) & object> : unknown) & { subscribe: import("/home/runner/work/matchina/matchina/dist/index").SubscribeFunc<Parameters<(import("/home/runner/work/matchina/matchina/dist/factory-machine-types").FactoryMachine<FC> & (FC extends FactoryMachineContext<any> ? import("/home/runner/work/matchina/matchina/dist/utility-types").DrainOuterGeneric<(object & import("/home/runner/work/matchina/matchina/dist/utility-types").TUnionToIntersection<import("/home/runner/work/matchina/matchina/dist/utility-types").FlatMemberUnion<import("/home/runner/work/matchina/matchina/dist/factory-machine-api-types").StateEventTransitionSenders<FC, keyof FC["transitions"]>>> extends infer T_1 ? { [K in keyof T_1]: T_1[K]; } : never) & object> : unknown))["notify"]>[0]>;}
matchina } from "matchina";
const const states: StateMatchboxFactory<{ Idle: undefined; Playing: (trackId: string) => { trackId: string; }; Paused: (trackId: string) => { trackId: string; }; Stopped: undefined;}>
states = defineStates<{ Idle: undefined; Playing: (trackId: string) => { trackId: string; }; Paused: (trackId: string) => { trackId: string; }; Stopped: undefined;}>(config: { Idle: undefined; Playing: (trackId: string) => { trackId: string; }; Paused: (trackId: string) => { trackId: string; }; Stopped: undefined;}): StateMatchboxFactory<{ Idle: undefined; Playing: (trackId: string) => { trackId: string; }; Paused: (trackId: string) => { trackId: string; }; Stopped: undefined;}>
defineStates({ type Idle: undefined
Idle: var undefined
undefined, type Playing: (trackId: string) => { trackId: string;}
Playing: (trackId: string
trackId: string) => ({ trackId: string
trackId }), type Paused: (trackId: string) => { trackId: string;}
Paused: (trackId: string
trackId: string) => ({ trackId: string
trackId }), type Stopped: undefined
Stopped: var undefined
undefined,});
// ✅ GOOD PRACTICE: Full type inference with inline definitionsconst const machine: FactoryMachine<{ states: StateMatchboxFactory<{ Idle: undefined; Playing: (trackId: string) => { trackId: string; }; Paused: (trackId: string) => { trackId: string; }; Stopped: undefined; }>; transitions: { Idle: { start: "Playing"; }; Playing: { pause: "Paused"; stop: "Stopped"; }; Paused: { resume: "Playing"; }; };}> & { start: (trackId: string) => void; pause: (trackId: string) => void; stop: () => void; resume: (trackId: string) => void;} & object & { subscribe: SubscribeFunc<FactoryMachineEvent<{ states: StateMatchboxFactory<{ Idle: undefined; Playing: (trackId: string) => { trackId: string; }; Paused: (trackId: string) => { trackId: string; }; Stopped: undefined; }>; transitions: { Idle: { start: "Playing"; }; Playing: { pause: "Paused"; stop: "Stopped"; }; Paused: { resume: "Playing"; }; }; }>>;}
machine = matchina<StateMatchboxFactory<{ Idle: undefined; Playing: (trackId: string) => { trackId: string; }; Paused: (trackId: string) => { trackId: string; }; Stopped: undefined;}>, { Idle: { start: "Playing"; }; Playing: { pause: "Paused"; stop: "Stopped"; }; Paused: { resume: "Playing"; };}, { states: StateMatchboxFactory<{ Idle: undefined; Playing: (trackId: string) => { trackId: string; }; Paused: (trackId: string) => { trackId: string; }; Stopped: undefined; }>; transitions: { Idle: { start: "Playing"; }; Playing: { pause: "Paused"; stop: "Stopped"; }; Paused: { resume: "Playing"; }; };}>(states: StateMatchboxFactory<...>, transitions: { Idle: { start: "Playing"; }; Playing: { pause: "Paused"; stop: "Stopped"; }; Paused: { resume: "Playing"; };}, init: StateMatchbox<...> | ... 3 more ... | KeysWithZeroRequiredArgs<...>): FactoryMachine<...> & ... 2 more ... & { ...;}
matchina( const states: StateMatchboxFactory<{ Idle: undefined; Playing: (trackId: string) => { trackId: string; }; Paused: (trackId: string) => { trackId: string; }; Stopped: undefined;}>
states, { type Idle: { start: "Playing";}
Idle: { // TypeScript will auto-complete state names start: "Playing"
start: "Playing", }, type Playing: { pause: "Paused"; stop: "Stopped";}
Playing: { // TypeScript will error if the state doesn't exist pause: "Paused"
pause: "Paused", stop: "Stopped"
stop: "Stopped", }, type Paused: { resume: "Playing";}
Paused: { resume: "Playing"
resume: "Playing", }, }, "Idle");
const machine: FactoryMachine<{ states: StateMatchboxFactory<{ Idle: undefined; Playing: (trackId: string) => { trackId: string; }; Paused: (trackId: string) => { trackId: string; }; Stopped: undefined; }>; transitions: { Idle: { start: "Playing"; }; Playing: { pause: "Paused"; stop: "Stopped"; }; Paused: { resume: "Playing"; }; };}> & { start: (trackId: string) => void; pause: (trackId: string) => void; stop: () => void; resume: (trackId: string) => void;} & object & { subscribe: SubscribeFunc<FactoryMachineEvent<{ states: StateMatchboxFactory<{ Idle: undefined; Playing: (trackId: string) => { trackId: string; }; Paused: (trackId: string) => { trackId: string; }; Stopped: undefined; }>; transitions: { Idle: { start: "Playing"; }; Playing: { pause: "Paused"; stop: "Stopped"; }; Paused: { resume: "Playing"; }; }; }>>;}
machine.st start: (trackId: string) => void
art("track-123"); // Idle -> Playingstartstatesstop
// nice!// ❌ BAD PRACTICE: Standalone transitions have no typesconst const invalidTransitions: { Idle: { start: string; }; Playing: { pause: string; stop: string; }; Paused: { resume: string; };}
invalidTransitions = { type Idle: { start: string;}
Idle: { start: string
start: "Playing", }, type Playing: { pause: string; stop: string;}
Playing: { pause: string
pause: "Paused", stop: string
stop: "Stopped", }, type Paused: { resume: string;}
Paused: { resume: string
resume: "Playing", },};
const const invalidMachine: FactoryMachine<{ states: StateMatchboxFactory<{ Idle: undefined; Playing: (trackId: string) => { trackId: string; }; Paused: (trackId: string) => { trackId: string; }; Stopped: undefined; }>; transitions: any;}> & { [x: string]: ((...args: any[] | unknown[]) => void) & ((...args: any[] | unknown[]) => void) & ((...args: any[] | unknown[]) => void);} & object & { subscribe: SubscribeFunc<FactoryMachineTransitionEvent<{ states: StateMatchboxFactory<{ Idle: undefined; Playing: (trackId: string) => { trackId: string; }; Paused: (trackId: string) => { trackId: string; }; Stopped: undefined; }>; transitions: any; }, string, string, any>>;}
invalidMachine = matchina<StateMatchboxFactory<{ Idle: undefined; Playing: (trackId: string) => { trackId: string; }; Paused: (trackId: string) => { trackId: string; }; Stopped: undefined;}>, any, { states: StateMatchboxFactory<{ Idle: undefined; Playing: (trackId: string) => { trackId: string; }; Paused: (trackId: string) => { trackId: string; }; Stopped: undefined; }>; transitions: any;}>(states: StateMatchboxFactory<{ Idle: undefined; Playing: (trackId: string) => { trackId: string; }; Paused: (trackId: string) => { trackId: string; }; Stopped: undefined;}>, transitions: any, init: StateMatchbox<...> | ... 3 more ... | KeysWithZeroRequiredArgs<...>): FactoryMachine<...> & ... 2 more ... & { ...;}
matchina( const states: StateMatchboxFactory<{ Idle: undefined; Playing: (trackId: string) => { trackId: string; }; Paused: (trackId: string) => { trackId: string; }; Stopped: undefined;}>
states, const invalidTransitions: { Idle: { start: string; }; Playing: { pause: string; stop: string; }; Paused: { resume: string; };}
invalidTransitions as any, // invalid because not typed correctly "Idle");
const invalidMachine: FactoryMachine<{ states: StateMatchboxFactory<{ Idle: undefined; Playing: (trackId: string) => { trackId: string; }; Paused: (trackId: string) => { trackId: string; }; Stopped: undefined; }>; transitions: any;}> & { [x: string]: ((...args: any[] | unknown[]) => void) & ((...args: any[] | unknown[]) => void) & ((...args: any[] | unknown[]) => void);} & object & { subscribe: SubscribeFunc<FactoryMachineTransitionEvent<{ states: StateMatchboxFactory<{ Idle: undefined; Playing: (trackId: string) => { trackId: string; }; Paused: (trackId: string) => { trackId: string; }; Stopped: undefined; }>; transitions: any; }, string, string, any>>;}
invalidMachine; // never
// Workaround: use `satisfies` to type transitionsconst const transitionsWithSatisfies: { Idle: { start: "Playing"; }; Playing: { pause: "Paused"; stop: "Stopped"; replay: () => (ev: ResolveEvent<FactoryMachineTransitionEvent<{ states: StateMatchboxFactory<{ Idle: undefined; Playing: (trackId: string) => { trackId: string; }; Paused: (trackId: string) => { trackId: string; }; Stopped: undefined; }>; transitions: any; }, string, string, any>> & { type: string; from: StateMatchbox<"Playing", { Idle: undefined; Playing: (trackId: string) => { trackId: string; }; Paused: (trackId: string) => { trackId: string; }; Stopped: undefined; }>; }) => StateMatchbox<...>; }; Paused: { ...; };}
transitionsWithSatisfies = { Idle?: { [x: string]: FactoryMachineTransition<StateMatchboxFactory<{ Idle: undefined; Playing: (trackId: string) => { trackId: string; }; Paused: (trackId: string) => { trackId: string; }; Stopped: undefined; }>, "Idle", string> | undefined;} | undefined
Idle: { start: "Playing"
start: "Playing", }, Playing?: { [x: string]: FactoryMachineTransition<StateMatchboxFactory<{ Idle: undefined; Playing: (trackId: string) => { trackId: string; }; Paused: (trackId: string) => { trackId: string; }; Stopped: undefined; }>, "Playing", string> | undefined;} | undefined
Playing: { pause: "Paused"
pause: "Paused", stop: "Stopped"
stop: "Stopped", replay: () => (ev: ResolveEvent<FactoryMachineTransitionEvent<{ states: StateMatchboxFactory<{ Idle: undefined; Playing: (trackId: string) => { trackId: string; }; Paused: (trackId: string) => { trackId: string; }; Stopped: undefined; }>; transitions: any;}, string, string, any>> & { type: string; from: StateMatchbox<"Playing", { Idle: undefined; Playing: (trackId: string) => { trackId: string; }; Paused: (trackId: string) => { trackId: string; }; Stopped: undefined; }>;}) => StateMatchbox<"Playing", { ...;}>
replay: () => (ev: ResolveEvent<FactoryMachineTransitionEvent<{ states: StateMatchboxFactory<{ Idle: undefined; Playing: (trackId: string) => { trackId: string; }; Paused: (trackId: string) => { trackId: string; }; Stopped: undefined; }>; transitions: any;}, string, string, any>> & { type: string; from: StateMatchbox<"Playing", { Idle: undefined; Playing: (trackId: string) => { trackId: string; }; Paused: (trackId: string) => { trackId: string; }; Stopped: undefined; }>;}
ev) => const states: StateMatchboxFactory<{ Idle: undefined; Playing: (trackId: string) => { trackId: string; }; Paused: (trackId: string) => { trackId: string; }; Stopped: undefined;}>
states.type Playing: (trackId: string) => StateMatchbox<"Playing", { Idle: undefined; Playing: (trackId: string) => { trackId: string; }; Paused: (trackId: string) => { trackId: string; }; Stopped: undefined;}>
Playing(ev: ResolveEvent<FactoryMachineTransitionEvent<{ states: StateMatchboxFactory<{ Idle: undefined; Playing: (trackId: string) => { trackId: string; }; Paused: (trackId: string) => { trackId: string; }; Stopped: undefined; }>; transitions: any;}, string, string, any>> & { type: string; from: StateMatchbox<"Playing", { Idle: undefined; Playing: (trackId: string) => { trackId: string; }; Paused: (trackId: string) => { trackId: string; }; Stopped: undefined; }>;}
ev.from: any
from.any
data.any
trackId), }, Paused?: { [x: string]: FactoryMachineTransition<StateMatchboxFactory<{ Idle: undefined; Playing: (trackId: string) => { trackId: string; }; Paused: (trackId: string) => { trackId: string; }; Stopped: undefined; }>, "Paused", string> | undefined;} | undefined
Paused: { resume: "Playing"
resume: "Playing", },} satisfies type FactoryMachineTransitions<SF extends KeyedStateFactory> = { [FromStateKey in keyof SF]?: { [x: string]: FactoryMachineTransition<SF, FromStateKey, string> | undefined;} | undefined; }
FactoryMachineTransitions<typeof const states: StateMatchboxFactory<{ Idle: undefined; Playing: (trackId: string) => { trackId: string; }; Paused: (trackId: string) => { trackId: string; }; Stopped: undefined;}>
states>;
const const validMachine: FactoryMachine<{ states: StateMatchboxFactory<{ Idle: undefined; Playing: (trackId: string) => { trackId: string; }; Paused: (trackId: string) => { trackId: string; }; Stopped: undefined; }>; transitions: { Idle: { start: "Playing"; }; Playing: { pause: "Paused"; stop: "Stopped"; replay: () => (ev: ResolveEvent<FactoryMachineTransitionEvent<{ states: StateMatchboxFactory<{ Idle: undefined; Playing: (trackId: string) => { trackId: string; }; Paused: (trackId: string) => { trackId: string; }; Stopped: undefined; }>; transitions: any; }, string, string, any>> & { type: string; from: StateMatchbox<"Playing", { Idle: undefined; Playing: (trackId: string) => { trackId: string; }; Paused: (trackId: string) => { trackId: string; }; Stopped: undefined; }>; }) => StateMatchbox<...>; }; Paused: { ...; }; };}> & { ...;} & object & { ...;}
validMachine = matchina<StateMatchboxFactory<{ Idle: undefined; Playing: (trackId: string) => { trackId: string; }; Paused: (trackId: string) => { trackId: string; }; Stopped: undefined;}>, { Idle: { start: "Playing"; }; Playing: { pause: "Paused"; stop: "Stopped"; replay: () => (ev: ResolveEvent<FactoryMachineTransitionEvent<{ states: StateMatchboxFactory<{ Idle: undefined; Playing: (trackId: string) => { trackId: string; }; Paused: (trackId: string) => { trackId: string; }; Stopped: undefined; }>; transitions: any; }, string, string, any>> & { ...; }) => StateMatchbox<...>; }; Paused: { ...; };}, { ...;}>(states: StateMatchboxFactory<...>, transitions: { Idle: { start: "Playing"; }; Playing: { pause: "Paused"; stop: "Stopped"; replay: () => (ev: ResolveEvent<FactoryMachineTransitionEvent<{ states: StateMatchboxFactory<{ Idle: undefined; Playing: (trackId: string) => { trackId: string; }; Paused: (trackId: string) => { trackId: string; }; Stopped: undefined; }>; transitions: any; }, string, string, any>> & { ...; }) => StateMatchbox<...>; }; Paused: { ...; };}, init: StateMatchbox<...> | ... 3 more ... | KeysWithZeroRequiredArgs<...>): FactoryMachine<...> & ... 2 more ... & { ...;}
matchina(const states: StateMatchboxFactory<{ Idle: undefined; Playing: (trackId: string) => { trackId: string; }; Paused: (trackId: string) => { trackId: string; }; Stopped: undefined;}>
states, const transitionsWithSatisfies: { Idle: { start: "Playing"; }; Playing: { pause: "Paused"; stop: "Stopped"; replay: () => (ev: ResolveEvent<FactoryMachineTransitionEvent<{ states: StateMatchboxFactory<{ Idle: undefined; Playing: (trackId: string) => { trackId: string; }; Paused: (trackId: string) => { trackId: string; }; Stopped: undefined; }>; transitions: any; }, string, string, any>> & { type: string; from: StateMatchbox<"Playing", { Idle: undefined; Playing: (trackId: string) => { trackId: string; }; Paused: (trackId: string) => { trackId: string; }; Stopped: undefined; }>; }) => StateMatchbox<...>; }; Paused: { ...; };}
transitionsWithSatisfies, "Idle");
const validMachine: FactoryMachine<{ states: StateMatchboxFactory<{ Idle: undefined; Playing: (trackId: string) => { trackId: string; }; Paused: (trackId: string) => { trackId: string; }; Stopped: undefined; }>; transitions: { Idle: { start: "Playing"; }; Playing: { pause: "Paused"; stop: "Stopped"; replay: () => (ev: ResolveEvent<FactoryMachineTransitionEvent<{ states: StateMatchboxFactory<{ Idle: undefined; Playing: (trackId: string) => { trackId: string; }; Paused: (trackId: string) => { trackId: string; }; Stopped: undefined; }>; transitions: any; }, string, string, any>> & { type: string; from: StateMatchbox<"Playing", { Idle: undefined; Playing: (trackId: string) => { trackId: string; }; Paused: (trackId: string) => { trackId: string; }; Stopped: undefined; }>; }) => StateMatchbox<...>; }; Paused: { ...; }; };}> & { ...;} & object & { ...;}
validMachine.st start: (trackId: string) => void
art("track-123"); // Idle -> Playingstartstatesstop
// correctly typedFollowing these practices will help you catch errors earlier, write more reliable code, and get the most out of Matchina’s TypeScript integration.