From da87e9b38427edb930904760fef76e319e7b3eba Mon Sep 17 00:00:00 2001 From: Gabriel Valfridsson Date: Thu, 3 Sep 2026 23:25:37 +0200 Subject: [PATCH] refactor(signals): preserve supplied store types --- .changeset/remove-store-readonly.md | 6 ++++ packages/signals/src/store/store.ts | 4 +-- .../signals/tests/store/createStore.test.ts | 1 - .../signals/tests/store/store.type-tests.ts | 36 +++++++++++++++++-- 4 files changed, 42 insertions(+), 5 deletions(-) create mode 100644 .changeset/remove-store-readonly.md diff --git a/.changeset/remove-store-readonly.md b/.changeset/remove-store-readonly.md new file mode 100644 index 000000000..1d40f278e --- /dev/null +++ b/.changeset/remove-store-readonly.md @@ -0,0 +1,6 @@ +--- +"@solidjs/signals": patch +"solid-js": patch +--- + +Preserve the supplied type in `Store` instead of adding a shallow readonly mapping. diff --git a/packages/signals/src/store/store.ts b/packages/signals/src/store/store.ts index 2451f60e4..14ed794bd 100644 --- a/packages/signals/src/store/store.ts +++ b/packages/signals/src/store/store.ts @@ -4,8 +4,8 @@ import type { Refreshable } from "../core/index.js"; import { GlobalQueue } from "../core/scheduler.js"; import { storeNextLookup } from "./next/target.js"; -/** A read-only view of a store's value as seen by consumers. Mutate it via the paired `StoreSetter`. */ -export type Store = Readonly; +/** A reactive view of a store's value. Update it through the paired `StoreSetter`. */ +export type Store = T; /** * A store setter. The callback receives a writable **draft** of the store. * diff --git a/packages/signals/tests/store/createStore.test.ts b/packages/signals/tests/store/createStore.test.ts index 7daa0c031..34099a3bd 100644 --- a/packages/signals/tests/store/createStore.test.ts +++ b/packages/signals/tests/store/createStore.test.ts @@ -25,7 +25,6 @@ describe("State immutability", () => { test("Setting a property", () => { const [state] = createStore({ name: "John" }); expect(state.name).toBe("John"); - // @ts-expect-error can't write readonly property state.name = "Jake"; expect(state.name).toBe("John"); }); diff --git a/packages/signals/tests/store/store.type-tests.ts b/packages/signals/tests/store/store.type-tests.ts index 9befde120..56d83015b 100644 --- a/packages/signals/tests/store/store.type-tests.ts +++ b/packages/signals/tests/store/store.type-tests.ts @@ -12,8 +12,40 @@ import { const [store, setStore] = createStore({ name: "John", age: 30 }); store.name satisfies string; store.age satisfies number; - // @ts-expect-error readonly - store.name = "Jake"; +} + +// ── stores preserve the supplied type ──────────────────────────────── + +{ + const [source] = createStore([{ id: 1 }]); + const [store, setStore] = createStore(source); + setStore(draft => { + draft.push({ id: 2 }); + }); + store satisfies { id: number }[]; +} + +{ + type List = { readonly id: string; items: { title: string }[] }; + const [list] = createStore({ id: "todos", items: [] }); + const [store, setStore] = createStore({ list }); + setStore(draft => { + draft.list.items = [{ title: "Review PR" }]; + // @ts-expect-error User-authored readonly remains readonly through nesting. + draft.list.id = "done"; + }); + store satisfies { list: List }; +} + +{ + type State = { readonly id: number; name: string }; + const [store, setStore] = createStore({ id: 1, name: "John" }); + store satisfies State; + setStore(draft => { + draft.name = "Jane"; + // @ts-expect-error User-authored readonly properties remain readonly. + draft.id = 2; + }); } // ── createStore (projection) — seed matches return type ───────────────