Skip to content
Version: XState v4

Troubleshooting

There are some known limitations with XState and TypeScript. We love TypeScript, and we’re constantly pressing ahead to make it a better experience in XState.

Here are some known issues, all of which have workarounds:

Events in machine options

When you use createMachine, you can pass in implementations to named actions, actors and guards in your config. For example:

import { createMachine } from 'xstate';

interface Context {}

type Event =
| { type: 'EVENT_WITH_FLAG'; flag: boolean }
| {
type: 'EVENT_WITHOUT_FLAG';
};

createMachine(
{
schema: {
context: {} as Context,
events: {} as Event,
},
on: {
EVENT_WITH_FLAG: {
actions: 'consoleLogData',
},
},
},
{
actions: {
consoleLogData: (context, event) => {
// This will error at .flag
console.log(event.flag);
},
},
},
);

The example above errors because inside the consoleLogData function, XState doesn’t know which event caused it to fire. The cleanest way to manage this issue is to assert the event type yourself:

import { createMachine } from 'xstate';

interface Context {}

type Event =
| { type: 'EVENT_WITH_FLAG'; flag: boolean }
| {
type: 'EVENT_WITHOUT_FLAG';
};

const config = {
schema: {
context: {} as Context,
events: {} as Event,
},
on: {
EVENT_WITH_FLAG: {
actions: 'consoleLogData',
},
},
};

createMachine(config, {
actions: {
consoleLogData: (context, event) => {
if (event.type !== 'EVENT_WITH_FLAG') return;
console.log(event.flag);
},
},
});

Sometimes it’s also possible to move the implementation inline.

import { createMachine } from 'xstate';
interface Context {}

type Event =
| { type: 'EVENT_WITH_FLAG'; flag: boolean }
| {
type: 'EVENT_WITHOUT_FLAG';
};

createMachine({
schema: {
context: {} as Context,
events: {} as Event,
},
on: {
EVENT_WITH_FLAG: {
actions: (context, event) => {
console.log(event.flag);
},
},
},
});

Moving the implementation inline doesn’t work for all cases. The action loses its name, making it uglier in the Visualizer. And if the action is duplicated in several places, you’ll need to copy-paste it to all the required locations.

Event types in entry actions

Event types in inline entry actions are not currently typed to the event that led to them. Consider the following example:

import { createMachine } from 'xstate';

interface Context {}

type Event =
| { type: 'EVENT_WITH_FLAG'; flag: boolean }
| {
type: 'EVENT_WITHOUT_FLAG';
};

createMachine({
schema: {
context: {} as Context,
events: {} as Event,
},
initial: 'state1',
states: {
state1: {
on: {
EVENT_WITH_FLAG: {
target: 'state2',
},
},
},
state2: {
entry: [
(context, event) => {
console.log(event.flag);
},
],
},
},
});

In the example above, XState doesn’t know which event led to the entry action on state2. The only fix similar to the fix for events in machine options above:

import { createMachine } from 'xstate';

interface Context {}

type Event =
| { type: 'EVENT_WITH_FLAG'; flag: boolean }
| {
type: 'EVENT_WITHOUT_FLAG';
};

createMachine({
schema: {
context: {} as Context,
events: {} as Event,
},
initial: 'state1',
states: {
state1: {
on: {
EVENT_WITH_FLAG: {
target: 'state2',
},
},
},
state2: {
entry: [
(context, event) => {
if (event.type !== 'EVENT_WITH_FLAG') return;
console.log(event.flag);
},
],
},
},
});

Assign action behaving strangely

When run in strict: true mode, assign actions can sometimes behave strangely.

import { createMachine, assign } from 'xstate';

interface Context {
something: boolean;
skip: boolean;
}

createMachine({
schema: {
context: {} as Context,
},
entry: [
assign({
skip: true,
something: (context) => context.something,
}),
],
});

In this case, it may appear that nothing you try works and all syntaxes seem buggy. The fix is strange but works consistently: add an unused context argument to the first argument of your assigner function.

import { createMachine, assign } from 'xstate';

interface Context {
something: boolean;
skip: boolean;
}

createMachine({
schema: {
context: {} as Context,
},
entry: [
assign({
skip: (context) => true,
something: (context) => context.something,
}),
],
});

The assign action issue is a nasty bug to fix and involves moving our codebase to strict mode, which we have planned for XState V5.

keyofStringsOnly

If you are seeing the following error:


Type error: Type 'string | number' does not satisfy the constraint 'string'.
Type 'number' is not assignable to type 'string'. TS2344

Ensure that your tsconfig file does not include "keyofStringsOnly": true,.

Config objects

The generic types for MachineConfig<TContext, any, TEvent> are the same as those for createMachine<TContext, TEvent>, which is useful when you are defining a machine config object outside of the createMachine(...) function, and helps prevent inference errors:

import { MachineConfig } from 'xstate';

const myMachineConfig: MachineConfig<TContext, any, TEvent> = {
id: 'controller',
initial: 'stopped',
states: {
stopped: {
/* ... */
},
started: {
/* ... */
},
},
// ...
};