Skip to content

fix: expose .snapshot() type on nested proxy objects#64

Merged
Moon-DaeSeung merged 3 commits into
mainfrom
fix/snapshot-nested-types
Apr 24, 2026
Merged

fix: expose .snapshot() type on nested proxy objects#64
Moon-DaeSeung merged 3 commits into
mainfrom
fix/snapshot-nested-types

Conversation

@Moon-DaeSeung
Copy link
Copy Markdown
Contributor

Summary

  • Make Snapshotable<T> recursive so every nested plain object/array proxy exposes .snapshot() at the type level (matches the existing runtime behavior in the proxy get trap).
  • Apply the same recursion to BoundResourceState<T>, so the action state() accessor types match.
  • Export snapshot and Snapshotable from the public API.
  • Bumps version to 0.3.3.

Why

Passing a nested proxy slice to a React Server Action threw Only plain objects can be passed to Server Functions ... Objects with symbol properties like proxy are not supported. The recommended fix is to call .snapshot() first — but .snapshot() was only typed on the top-level proxy, even though the runtime proxy get trap returns it for every nested object. Callers had to cast (as Snapshotable<T>) which defeats the type system.

// Before — type error
const filter = state.filter.snapshot()
// After — natural inference
const filter = state.filter.snapshot()

Array element types are intentionally not recursed into, so Array.prototype.push, [i] = …, etc. keep accepting the plain element type. For array elements, use the standalone snapshot(state.items[0]) helper.

Test plan

  • tests/snapshot-types.test.ts — 7 type-level tests covering top-level / nested object / array .snapshot(), array push compatibility, standalone helper, function pass-through, and the action(({ state }) => …) accessor.
  • Full suite: 293 tests passing.
  • tsc -p tsconfig.json clean.

🤖 Generated with Claude Code

`Snapshotable<T>` only added `.snapshot()` to the top-level proxy, even
though the runtime proxy `get` trap returns it for every nested object.
Callers passing a nested slice to a server action (e.g. `state.filter`)
hit a TS error and had to cast.

Make `Snapshotable<T>` recursive — every plain object/array layer gets
`.snapshot()`. Same fix applied to `BoundResourceState<T>` so the action
`state()` accessor types match. Array element types are intentionally
not recursed into so `Array.push` etc. keep accepting plain elements.

Also exports `snapshot` and `Snapshotable` from the public API.

Bumps version to 0.3.3.
@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 20, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
comwit-docs Ready Ready Preview Apr 20, 2026 4:34am

The recursive `Snapshotable<T>` and `BoundResourceState<T>` were adding
`.snapshot()` to every object-typed field, including built-ins like
`Date` and `Map`. The runtime proxy never wraps those (canProxy() is
false for non-Object.prototype prototypes), so the decoration broke
assignments — `this.model.user = userFromApi` failed because plain
`Date` doesn't satisfy `Date & { snapshot(): Date }`.

Exclude common built-in object types (`Date`, `RegExp`, `Error`,
`Promise`, `Map`, `Set`, `WeakMap`, `WeakSet`, `ArrayBuffer`,
`DataView`) from the recursion so plain values remain assignable.

Bumps to 0.3.4.
Recursive `T & { snapshot(): T }` made plain values un-assignable to
state fields (`state.user = userFromApi` failed). Excluding only built-in
objects didn't help — any user-defined plain type (User, ProjectDetail)
hit the same wall.

Revert to top-level-only `Snapshotable<T>` on `createProxy`. For nested
slices, the proxy `get` trap still intercepts `.snapshot()` at runtime;
at the type level use the standalone `snapshot(state.slice)` helper.

Also bumps to 0.3.5 and updates llms.txt with the new pattern.
@Moon-DaeSeung Moon-DaeSeung merged commit 5615348 into main Apr 24, 2026
1 check passed
@Moon-DaeSeung Moon-DaeSeung deleted the fix/snapshot-nested-types branch April 24, 2026 01:09
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.

1 participant