Documentation Index
Fetch the complete documentation index at: https://mintlify.com/statelyai/xstate/llms.txt
Use this file to discover all available pages before exploring further.
XState is written in TypeScript and provides comprehensive type safety for state machines, actors, and all related APIs. This guide covers advanced TypeScript patterns for working with XState.
Typing Machines
XState v5 uses TypeScript’s type inference to provide strong typing without excessive type annotations. The machine definition itself infers most types automatically.
import { createMachine, assign } from 'xstate';
const toggleMachine = createMachine({
types: {} as {
context: { count: number };
events:
| { type: 'TOGGLE' }
| { type: 'INCREMENT'; value: number };
},
id: 'toggle',
initial: 'inactive',
context: { count: 0 },
states: {
inactive: {
on: {
TOGGLE: 'active'
}
},
active: {
entry: assign({
count: ({ context, event }) => context.count + 1
}),
on: {
TOGGLE: 'inactive',
INCREMENT: {
actions: assign({
count: ({ context, event }) => context.count + event.value
})
}
}
}
}
});
Using setup() for Better Type Inference
The setup() function provides the best TypeScript experience by allowing you to define types, actions, guards, and actors in one place:
import { setup, assign } from 'xstate';
const machine = setup({
types: {} as {
context: { count: number; user: { name: string } | null };
events:
| { type: 'INCREMENT' }
| { type: 'DECREMENT' }
| { type: 'SET_USER'; user: { name: string } };
input: { initialCount: number };
output: { finalCount: number };
},
actions: {
logCount: ({ context }) => {
console.log('Count:', context.count);
},
notifyUser: ({ context }, params: { message: string }) => {
console.log(`${context.user?.name}: ${params.message}`);
}
},
guards: {
isPositive: ({ context }) => context.count > 0,
hasUser: ({ context }) => context.user !== null
}
}).createMachine({
id: 'counter',
initial: 'counting',
context: ({ input }) => ({
count: input.initialCount,
user: null
}),
states: {
counting: {
on: {
INCREMENT: {
actions: [
assign({ count: ({ context }) => context.count + 1 }),
'logCount'
]
},
DECREMENT: {
guard: 'isPositive',
actions: assign({ count: ({ context }) => context.count - 1 })
},
SET_USER: {
actions: assign({ user: ({ event }) => event.user })
}
}
}
},
output: ({ context }) => ({ finalCount: context.count })
});
The types property uses TypeScript’s as assertion with an empty object. This is a type-only construct that doesn’t affect runtime behavior.
XState provides utility types to extract types from machines and actors:
SnapshotFrom<T>
Extracts the snapshot type from a machine or actor logic:
import { SnapshotFrom } from 'xstate';
type ToggleSnapshot = SnapshotFrom<typeof toggleMachine>;
// ToggleSnapshot includes: value, context, status, etc.
function logSnapshot(snapshot: ToggleSnapshot) {
console.log(snapshot.value); // 'active' | 'inactive'
console.log(snapshot.context.count); // number
}
ActorRefFrom<T>
Extracts the actor ref type:
import { ActorRefFrom } from 'xstate';
type ToggleActorRef = ActorRefFrom<typeof toggleMachine>;
function useToggle(actorRef: ToggleActorRef) {
actorRef.send({ type: 'TOGGLE' });
const snapshot = actorRef.getSnapshot();
}
Extract event and input types:
import { EventFrom, InputFrom } from 'xstate';
type ToggleEvent = EventFrom<typeof toggleMachine>;
// { type: 'TOGGLE' } | { type: 'INCREMENT'; value: number }
type ToggleInput = InputFrom<typeof machine>;
// { initialCount: number }
Typing Actions
Actions can be strongly typed using the ActionFunction type:
import { ActionFunction } from 'xstate';
type Context = { count: number };
type Event = { type: 'INCREMENT'; value: number };
const incrementAction: ActionFunction<
Context,
Event,
Event,
{ by: number }, // params
any, // TActor
any, // TAction
any, // TGuard
any, // TDelay
any // TEmitted
> = ({ context, event }, params) => {
console.log(`Incrementing by ${params.by}`);
};
When using setup(), action types are inferred automatically. Manual typing is only needed for standalone actions.
Typing Guards
Guards use the Guard or GuardPredicate types:
import { GuardPredicate } from 'xstate';
type Context = { count: number };
type Event = { type: 'CHECK' };
const isEven: GuardPredicate<Context, Event> = ({ context }) => {
return context.count % 2 === 0;
};
Typing Actors
Child actors can be strongly typed using the ProvidedActor interface:
import { setup, fromPromise } from 'xstate';
const fetchUser = fromPromise(async ({ input }: { input: { userId: string } }) => {
const response = await fetch(`/api/users/${input.userId}`);
return response.json() as Promise<{ id: string; name: string }>;
});
const machine = setup({
types: {} as {
context: { user: { id: string; name: string } | null };
events: { type: 'FETCH'; userId: string };
},
actors: {
fetchUser
}
}).createMachine({
initial: 'idle',
context: { user: null },
states: {
idle: {
on: {
FETCH: 'loading'
}
},
loading: {
invoke: {
src: 'fetchUser',
input: ({ event }) => ({ userId: event.userId }),
onDone: {
target: 'success',
actions: assign({
user: ({ event }) => event.output // fully typed!
})
},
onError: 'failure'
}
},
success: {},
failure: {}
}
});
Advanced Type Patterns
Discriminated Unions for Events
Use TypeScript’s discriminated unions for type-safe event handling:
type Event =
| { type: 'SUBMIT'; data: { email: string; password: string } }
| { type: 'CANCEL' }
| { type: 'RETRY'; attemptNumber: number };
const machine = createMachine({
types: {} as { events: Event },
// ...
states: {
form: {
on: {
SUBMIT: {
actions: ({ event }) => {
// event.data is available and typed
console.log(event.data.email);
}
},
RETRY: {
actions: ({ event }) => {
// event.attemptNumber is available and typed
console.log(event.attemptNumber);
}
}
}
}
}
});
Type-Safe State Matching
Use type predicates for state matching:
import { SnapshotFrom } from 'xstate';
type Snapshot = SnapshotFrom<typeof machine>;
function isLoadingState(snapshot: Snapshot): snapshot is Snapshot & { value: 'loading' } {
return snapshot.matches('loading');
}
const snapshot = actor.getSnapshot();
if (isLoadingState(snapshot)) {
// TypeScript knows we're in loading state
console.log('Loading...');
}
Conditional Context Types
Different states can have different context shapes:
type Context =
| { status: 'idle'; data: null; error: null }
| { status: 'loading'; data: null; error: null }
| { status: 'success'; data: string[]; error: null }
| { status: 'failure'; data: null; error: Error };
const machine = createMachine({
types: {} as { context: Context },
initial: 'idle',
context: { status: 'idle', data: null, error: null },
states: {
idle: {},
loading: {
entry: assign({ status: 'loading' })
},
success: {
entry: assign(({ event }: any) => ({
status: 'success',
data: event.output,
error: null
}))
},
failure: {
entry: assign(({ event }: any) => ({
status: 'failure',
data: null,
error: event.error
}))
}
}
});
Avoid using any in production code. The examples above use any for brevity, but you should always provide explicit types.
Common Type Utilities
XState exports many utility types from types.ts:1-2000:
MachineContext - Base type for machine context
EventObject - Base type for events
StateValue - Type for state values (string or nested object)
ActorRef - Reference to an actor
ActorRefFrom<T> - Extract actor ref type
SnapshotFrom<T> - Extract snapshot type
EventFrom<T> - Extract event type
InputFrom<T> - Extract input type
OutputFrom<T> - Extract output type
Troubleshooting
Type inference not working
If TypeScript can’t infer types:
- Use
setup() instead of createMachine() directly
- Ensure
types property is defined at the top level
- Check that TypeScript version is 5.0 or higher
Circular type references
If you encounter circular type references:
// Use type aliases to break the cycle
type MyContext = { count: number };
type MyEvents = { type: 'INCREMENT' };
const machine = setup({
types: {} as {
context: MyContext;
events: MyEvents;
}
}).createMachine({ /* ... */ });
Param types not inferred
Params must be explicitly typed in setup():
setup({
actions: {
notify: ({}, params: { message: string }) => {
console.log(params.message);
}
}
});