Skip to content

TypeScript Inference Tips

To ensure the best developer experience with Matchina:

  1. Define your transitions inside your createMachine function call
  2. Avoid standalone transition objects: These break TypeScript’s ability to validate your code
  3. Let TypeScript help you: Embrace the rich type checking and autocomplete features

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

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.

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 definitions
const
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 -> Playing
start
states
stop
// nice!

Following these practices will help you catch errors earlier, write more reliable code, and get the most out of Matchina’s TypeScript integration.