Fix #5480: narrow ActorOptions.snapshot and getPersistedSnapshot() to SnapshotFrom<TLogic> - #5547
Open
ther12k wants to merge 2 commits into
Open
Fix #5480: narrow ActorOptions.snapshot and getPersistedSnapshot() to SnapshotFrom<TLogic>#5547ther12k wants to merge 2 commits into
ther12k wants to merge 2 commits into
Conversation
…SnapshotFrom<TLogic> Closes statelyai#5480. Both ActorOptions.snapshot and Actor.getPersistedSnapshot() were typed Snapshot<unknown> (a 4-shape union of status / output / error). Real machine snapshots (MachineSnapshot<...>) have a richer shape with value / context / matches() and do not overlap with Snapshot<unknown>, so the round-trip getPersistedSnapshot() -> useMachine(machine, { snapshot }) failed typecheck even though the runtime was fine. Narrow both sides to SnapshotFrom<TLogic> so the types agree. For StateMachine, SnapshotFrom<TMachine> resolves to MachineSnapshot<...> via ReturnType<R['transition']>, which is exactly what getPersistedSnapshot() returns at runtime. - packages/core/src/types.ts: ActorOptions.snapshot and ActorOptions.state use SnapshotFrom<TLogic>. - packages/core/src/createActor.ts: Actor.getPersistedSnapshot() return is SnapshotFrom<TLogic>. The internal this.logic.getPersistedSnapshot(...) call still returns Snapshot<unknown> per ActorLogic's generic declaration, so a single cast is applied at the public boundary. Runtime behaviour is unchanged. Backward-compatible: callers passing a hand-written Snapshot<unknown> are unaffected because the internal _initState / restoreSnapshot flow still accepts any shape that the logic's restoreSnapshot can convert. Adds a regression test in packages/xstate-react/test/types.test.tsx (statelyai#5480) that exercises the round-trip end-to-end without `as any` or `@ts-expect-error`. Two existing tests (createActorContext.test.tsx, useActor.test.tsx) updated to use SnapshotFrom<typeof machine> for their persistedState annotations to keep tsc green.
🦋 Changeset detectedLatest commit: ab963e3 The changes in this PR will be included in the next version bump. This PR includes changesets to release 2 packages
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
approved these changes
Jun 22, 2026
15 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #5480
Problem
ActorOptions.snapshotandActor.getPersistedSnapshot()were typedSnapshot<unknown>(a 4-shape union ofstatus/output/error). Real machine snapshots (MachineSnapshot<...>) have aricher shape (
status/value/context/matches(...)/...). The two shapes do not overlap, so the round-trip
getPersistedSnapshot()→useMachine(machine, { snapshot })failed typecheck even though the runtime is fine.
The deprecated
statealias had the same problem.Fix
Narrow both sides of the round-trip to
SnapshotFrom<TLogic>sothe types agree:
packages/core/src/types.ts—ActorOptions.snapshotandActorOptions.statenow useSnapshotFrom<TLogic>instead ofSnapshot<unknown>.packages/core/src/createActor.ts—Actor.getPersistedSnapshot()return type is nowSnapshotFrom<TLogic>. The internalthis.logic.getPersistedSnapshot(...)call still returnsSnapshot<unknown>perActorLogic's generic declaration, soa single cast is applied at the public boundary. Runtime
behaviour is unchanged.
For a
StateMachine,SnapshotFrom<TMachine>resolves toMachineSnapshot<...>viaReturnType<R['transition']>, whichis exactly what
getPersistedSnapshot()returns at runtime.Test
packages/xstate-react/test/types.test.tsxgets a newit('useMachine accepts a rehydrated snapshot from getPersistedSnapshot #5480', ...)that exercises the round-tripend-to-end and is expected to typecheck without
as anyor@ts-expect-error. Two existing tests(
createActorContext.test.tsx,useActor.test.tsx) are updatedto use
SnapshotFrom<typeof machine>for theirpersistedStateannotations to keep
tscgreen.Verification
pnpm typecheck→ 0 errors (3 errors before the test-fileannotations were updated; now clean).
pnpm test:coreis gated on Node ≥ 22 in this repo (usesPromise.withResolvers) — environment issue unrelated to thischange.
Backward-compatible: callers passing a hand-written
Snapshot<unknown>tosnapshot:are unaffected because theinternal
_initState/restoreSnapshotflow still accepts anyshape that the logic's
restoreSnapshotcan convert.