Skip to content

Type safe states#5478

Open
SandroMaglione wants to merge 11 commits intostatelyai:mainfrom
SandroMaglione:sandromaglione/type-safe-states
Open

Type safe states#5478
SandroMaglione wants to merge 11 commits intostatelyai:mainfrom
SandroMaglione:sandromaglione/type-safe-states

Conversation

@SandroMaglione
Copy link
Copy Markdown
Contributor

Two additions to createMachine's type signature:

  1. const TStates — a new type parameter that captures the literal shape of the states property via & { states?: TStates } (additive intersection, MachineConfig stays intact).

  2. ValidateConfigTargets<{ states: TStates }> — a validation type intersected into the config. It walks the state tree, extracts keyof TStates & string as sibling keys at each level, and constrains every target value in on, always, after, onDone, and invoke.onDone/onError/onSnapshot to ValidTarget<TSiblingKeys> (sibling names, .child, #id, sibling.child, or escaped-dot variants).

For setup().createMachine, the existing const TConfig already captures literals — only & ValidateConfigTargets<TConfig> is added.

How it works

ValidTarget<TSiblingKeys> defines valid patterns. ValidateConfigTargets recursively maps over the config, replacing each target position with ValidTarget<siblings>. The intersection with MachineConfig (which accepts string) narrows targets to valid siblings. Invalid literals produce a type error; spread-widened string values pass through via a string extends T ? T escape hatch.

What the TSiblingKeys threading is for

A TSiblingKeys extends string = string parameter was added to TransitionConfigOrTarget, TransitionsConfig, DelayedTransitions, InvokeConfig, StateNodeConfig, StatesConfig, and MachineConfig. All default to string (non-breaking). These replace bare string with ValidTarget<TSiblingKeys> in shorthand target positions. MachineConfig wraps it in DoNotInfer. This infrastructure supports the validation types but doesn't constrain anything on its own — ValidateConfigTargets does the actual enforcement.

Test coverage (typesafe-states.test.ts)

74 tests across createMachine and setup().createMachine. Every "should allow" has a matching "should reject" (@ts-expect-error):

  • on: bare string, object-form { target }, array of objects
  • always: bare string, object-form, array of objects
  • after: bare string, object-form
  • onDone (state node): bare string, object-form
  • invoke.onDone/invoke.onError: bare string, object-form
  • Target patterns: .child, #id, sibling.child, escaped dots ('foo\\.bar')
  • Nested states: sibling scoping at each level
  • Escape hatches: initial unconstrained, spread variables pass through

pnpm run typecheck and pnpm run test:core pass ☑️

@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Mar 8, 2026

🦋 Changeset detected

Latest commit: 271d473

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 5 packages
Name Type
xstate Patch
@xstate/react Patch
@xstate/solid Patch
@xstate/svelte Patch
@xstate/vue Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@davidkpiano davidkpiano requested a review from Andarist March 8, 2026 10:06
@davidkpiano
Copy link
Copy Markdown
Member

cc. @Andarist

@wmhina
Copy link
Copy Markdown

wmhina commented Mar 11, 2026

Very much needed functionality! Would it also validate below example correctly?

const machine = setup({...}).createMachine({
id: "my-machine",
initial: "STATE_A",
on: {
CANCEL: 'END', // Currently fails during runtime since at the root level there are no sibling states
ERROR: '.END', // Works at runtime
},
states: {
STATE_A: {},
END: { type: 'final' },
},
});

@SandroMaglione
Copy link
Copy Markdown
Contributor Author

SandroMaglione commented Mar 15, 2026

@wmhina added this check as well in 45a446b (edit: but only when using setup, not with just createMachine 0907838)

@SandroMaglione
Copy link
Copy Markdown
Contributor Author

@Andarist how can I make reviewing this PR easier? The changes are mostly all made to pass more type information such that states can be checked.

Anything that I can do to help (description, comments, etc.)?

@davidkpiano
Copy link
Copy Markdown
Member

@SandroMaglione Can you merge main into here and test again? Just updated TS version

@SandroMaglione SandroMaglione force-pushed the sandromaglione/type-safe-states branch from 0907838 to 56c3354 Compare April 7, 2026 16:53
@SandroMaglione
Copy link
Copy Markdown
Contributor Author

@SandroMaglione Can you merge main into here and test again? Just updated TS version

Done

@Andarist
Copy link
Copy Markdown
Collaborator

Andarist commented Apr 7, 2026

Fyi, im in the process of reviewing this

@Andarist
Copy link
Copy Markdown
Collaborator

@SandroMaglione this has type issues in tests right now, could you take a look at this?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants