TypeScript Inference Tips
To ensure the best developer experience with Matchina:
- Define your transitions inside your
createMachine
function 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` creates a type-safe state factory for your state machine.
Each key in the config becomes a state constructor, inferring parameters and data shape.
Usage:
```ts
const states = defineStates({
SomeEmptyState: undefined, // No parameters
SomeValueState: "any value here" || 42, // Any value as data
SomeStateWithCreate: (...anyParameters) => ({ ...anyPayload }),
});
// Usage
states.SomeEmptyState().key; // "SomeEmptyState"
states.SomeValueState().data; // "any value here" || 42
states.SomeStateWithCreate("param1", "param2").data; // { ...anyPayload }
```
Type benefits:
- State keys and data are fully inferred
- Pattern matching and type guards are available
- Exhaustive match on state keysdefineStates, function matchina<SF extends KeyedStateFactory, TC extends FactoryMachineTransitions<SF>, FC extends FactoryMachineContext<SF> = {
...;
}>(states: SF, transitions: TC, init: KeysWithZeroRequiredArgs<FC["states"]> | FactoryKeyedState<FC["states"]>): import("/Users/winston/dev/personal/matchina/dist/factory-machine-types").FactoryMachine<FC> & import("/Users/winston/dev/personal/matchina/dist/utility-types").DrainOuterGeneric<(object & import("/Users/winston/dev/personal/matchina/dist/utility-types").TUnionToIntersection<import("/Users/winston/dev/personal/matchina/dist/utility-types").FlatMemberUnion<import("/Users/winston/dev/personal/matchina/dist/factory-machine-api-types").StateEventTransitionSenders<import("/Users/winston/dev/personal/matchina/dist/factory-machine-types").FactoryMachine<FC>, keyof FC["transitions"]>>> extends infer T ? { [K in keyof T]: (object & import("/Users/winston/dev/personal/matchina/dist/utility-types").TUnionToIntersection<import("/Users/winston/dev/personal/matchina/dist/utility-types").FlatMemberUnion<import("/Users/winston/dev/personal/matchina/dist/factory-machine-api-types").StateEventTransitionSenders<import("/Users/winston/dev/personal/matchina/dist/factory-machine-types").FactoryMachine<FC>, keyof FC["transitions"]>>>)[K]; } : never) & object>
Creates a strongly-typed state machine using the provided states, transitions, and initial state.
Wraps the machine with additional utilities via `assignEventApi` for ergonomic usage.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` creates a type-safe state factory for your state machine.
Each key in the config becomes a state constructor, inferring parameters and data shape.
Usage:
```ts
const states = defineStates({
SomeEmptyState: undefined, // No parameters
SomeValueState: "any value here" || 42, // Any value as data
SomeStateWithCreate: (...anyParameters) => ({ ...anyPayload }),
});
// Usage
states.SomeEmptyState().key; // "SomeEmptyState"
states.SomeValueState().data; // "any value here" || 42
states.SomeStateWithCreate("param1", "param2").data; // { ...anyPayload }
```
Type benefits:
- State keys and data are fully inferred
- Pattern matching and type guards are available
- Exhaustive match on state keysdefineStates({
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: {
...;
};
}> & {
...;
} & object
machine = matchina<StateMatchboxFactory<{
Idle: undefined;
Playing: (trackId: string) => {
trackId: string;
};
Paused: (trackId: string) => {
trackId: string;
};
Stopped: undefined;
}>, {
...;
}, {
...;
}>(states: StateMatchboxFactory<...>, transitions: {
...;
}, init: StateMatchbox<...> | ... 3 more ... | KeysWithZeroRequiredArgs<...>): import("/Users/winston/dev/personal/matchina/dist/factory-machine-types").FactoryMachine<{
...;
}> & import("/Users/winston/dev/personal/matchina/dist/utility-types").DrainOuterGeneric<(object & import("/Users/winston/dev/personal/matchina/dist/utility-types").TUnionToIntersection<import("/Users/winston/dev/personal/matchina/dist/utility-types").FlatMemberUnion<import("/Users/winston/dev/personal/matchina/dist/factory-machine-api-types").StateEventTransitionSenders<import("/Users/winston/dev/personal/matchina/dist/factory-machine-types").FactoryMachine<{
...;
}>, "Idle" | ... 1 more ... | "Paused">>> extends infer T ? { [K in keyof T]: (object & import("/Users/winston/dev/personal/matchina/dist/utility-types").TUnionToIntersection<import("/Users/winston/dev/personal/matchina/dist/utility-types").FlatMemberUnion<import("/Users/winston/dev/personal/matchina/dist/factory-machine-api-types").StateEventTransitionSenders<import("/Users/winston/dev/personal/matchina/dist/factory-machine-types").FactoryMachine<{
...;
}>, "Idle" | ... 1 more ... | "Paused">>>)[K]; } : never) & object>
Creates a strongly-typed state machine using the provided states, transitions, and initial state.
Wraps the machine with additional utilities via `assignEventApi` for ergonomic usage.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: {
...;
};
}> & {
...;
} & object
machine.st- start
- states
- stop
start: (trackId: string) => void
art("track-123"); // Idle -> Playing
// nice!
// ❌ BAD PRACTICE: Standalone transitions have no types
const 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;
}> & {
...;
} & object
invalidMachine = matchina<StateMatchboxFactory<{
Idle: undefined;
Playing: (trackId: string) => {
trackId: string;
};
Paused: (trackId: string) => {
trackId: string;
};
Stopped: undefined;
}>, any, {
...;
}>(states: StateMatchboxFactory<...>, transitions: any, init: StateMatchbox<...> | ... 3 more ... | KeysWithZeroRequiredArgs<...>): import("/Users/winston/dev/personal/matchina/dist/factory-machine-types").FactoryMachine<{
...;
}> & import("/Users/winston/dev/personal/matchina/dist/utility-types").DrainOuterGeneric<(object & import("/Users/winston/dev/personal/matchina/dist/utility-types").TUnionToIntersection<import("/Users/winston/dev/personal/matchina/dist/utility-types").FlatMemberUnion<import("/Users/winston/dev/personal/matchina/dist/factory-machine-api-types").StateEventTransitionSenders<import("/Users/winston/dev/personal/matchina/dist/factory-machine-types").FactoryMachine<{
...;
}>, string | ... 1 more ... | symbol>>> extends infer T ? { [K in keyof T]: (object & import("/Users/winston/dev/personal/matchina/dist/utility-types").TUnionToIntersection<import("/Users/winston/dev/personal/matchina/dist/utility-types").FlatMemberUnion<import("/Users/winston/dev/personal/matchina/dist/factory-machine-api-types").StateEventTransitionSenders<import("/Users/winston/dev/personal/matchina/dist/factory-machine-types").FactoryMachine<{
...;
}>, string | ... 1 more ... | symbol>>>)[K]; } : never) & object>
Creates a strongly-typed state machine using the provided states, transitions, and initial state.
Wraps the machine with additional utilities via `assignEventApi` for ergonomic usage.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;
}> & {
...;
} & object
invalidMachine; // never
// Workaround: use `satisfies` to type transitions
const 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) => {
...;
};
Stopped: undefined;
}>;
transitions: any;
}, string, string, any>> & {
...;
}) => 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>> & {
...;
}) => StateMatchbox<...>
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>> & {
...;
}
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>> & {
...;
}
ev.from: any
from.data.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: {
...;
};
}> & {
...;
} & object
validMachine = matchina<StateMatchboxFactory<{
Idle: undefined;
Playing: (trackId: string) => {
trackId: string;
};
Paused: (trackId: string) => {
trackId: string;
};
Stopped: undefined;
}>, {
...;
}, {
...;
}>(states: StateMatchboxFactory<...>, transitions: {
...;
}, init: StateMatchbox<...> | ... 3 more ... | KeysWithZeroRequiredArgs<...>): import("/Users/winston/dev/personal/matchina/dist/factory-machine-types").FactoryMachine<{
...;
}> & import("/Users/winston/dev/personal/matchina/dist/utility-types").DrainOuterGeneric<(object & import("/Users/winston/dev/personal/matchina/dist/utility-types").TUnionToIntersection<import("/Users/winston/dev/personal/matchina/dist/utility-types").FlatMemberUnion<import("/Users/winston/dev/personal/matchina/dist/factory-machine-api-types").StateEventTransitionSenders<import("/Users/winston/dev/personal/matchina/dist/factory-machine-types").FactoryMachine<{
...;
}>, "Playing" | ... 1 more ... | "Idle">>> extends infer T ? { [K in keyof T]: (object & import("/Users/winston/dev/personal/matchina/dist/utility-types").TUnionToIntersection<import("/Users/winston/dev/personal/matchina/dist/utility-types").FlatMemberUnion<import("/Users/winston/dev/personal/matchina/dist/factory-machine-api-types").StateEventTransitionSenders<import("/Users/winston/dev/personal/matchina/dist/factory-machine-types").FactoryMachine<{
...;
}>, "Playing" | ... 1 more ... | "Idle">>>)[K]; } : never) & object>
Creates a strongly-typed state machine using the provided states, transitions, and initial state.
Wraps the machine with additional utilities via `assignEventApi` for ergonomic usage.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) => {
...;
};
Stopped: undefined;
}>;
transitions: any;
}, string, string, any>> & {
...;
}) => StateMatchbox<...>;
};
Paused: {
...;
};
}
transitionsWithSatisfies, "Idle");
const validMachine: FactoryMachine<{
states: StateMatchboxFactory<{
Idle: undefined;
Playing: (trackId: string) => {
trackId: string;
};
Paused: (trackId: string) => {
trackId: string;
};
Stopped: undefined;
}>;
transitions: {
...;
};
}> & {
...;
} & object
validMachine.st- start
- states
- stop
start: (trackId: string) => void
art("track-123"); // Idle -> Playing
// correctly typed
Following these practices will help you catch errors earlier, write more reliable code, and get the most out of Matchina’s TypeScript integration.