Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/require-complete-store-seeds.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@solidjs/signals": patch
"solid-js": patch
---

Require complete values for projection store seeds. `createProjection`, derived `createStore`, and derived `createOptimisticStore` no longer accept `Partial<T>`, preventing a store typed as `T` from being created without all required properties.
2 changes: 1 addition & 1 deletion packages/signals/src/store/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export function createStore<T extends object = {}>(
): [get: Store<T>, set: StoreSetter<T>];
export function createStore<T extends object = {}>(
fn: (store: T) => void | T | Promise<void | T> | AsyncIterable<void | T>,
store: Partial<T> | Store<NoFn<T>>,
store: NoFn<T> | Store<NoFn<T>>,
options?: ProjectionOptions
): [get: Refreshable<Store<T>>, set: StoreSetter<T>];
export function createStore(first: any, second?: any, third?: any): any {
Expand Down
8 changes: 8 additions & 0 deletions packages/signals/src/store/next/optimistic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,14 @@ function familyHasLiveOverrides(fam: { overlaid?: Set<any> }): boolean {
return false;
}

export function createOptimisticStoreNext<T extends object = {}>(
store: NoFn<T> | Store<NoFn<T>>
): [get: Store<T>, set: StoreSetter<T>];
export function createOptimisticStoreNext<T extends object = {}>(
fn: (store: T) => void | T | Promise<void | T> | AsyncIterable<void | T>,
store: NoFn<T> | Store<NoFn<T>>,
options?: ProjectionOptions
): [get: Store<T>, set: StoreSetter<T>];
export function createOptimisticStoreNext<T extends object = {}>(
first: T | ((store: T) => void | T | Promise<void | T> | AsyncIterable<void | T>),
second?: NoFn<T> | Store<NoFn<T>>,
Expand Down
6 changes: 3 additions & 3 deletions packages/signals/src/store/next/projection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ function wrapDraft(

function createProjectionNextInternal<T extends object = {}>(
fn: (draft: T) => void | T | Promise<void | T> | AsyncIterable<void | T>,
seed: Partial<T>,
seed: T,
options?: ProjectionOptions
) {
const fam: StoreNextFamily = {
Expand Down Expand Up @@ -211,7 +211,7 @@ function createProjectionNextInternal<T extends object = {}>(

export function createProjectionNext<T extends object = {}>(
fn: (draft: T) => void | T | Promise<void | T> | AsyncIterable<void | T>,
seed: Partial<T> | Store<NoFn<T>>,
seed: NoFn<T> | Store<NoFn<T>>,
options?: ProjectionOptions
): Refreshable<Store<T>> {
return createProjectionNextInternal(fn, seed, options).store;
Expand All @@ -222,7 +222,7 @@ export function createProjectionNext<T extends object = {}>(
* same-flush dependency change). */
export function createStoreDerivedNext<T extends object = {}>(
fn: (draft: T) => void | T | Promise<void | T> | AsyncIterable<void | T>,
seed: Partial<T> | Store<NoFn<T>>,
seed: NoFn<T> | Store<NoFn<T>>,
options?: ProjectionOptions
): [Refreshable<Store<T>>, (f: (draft: T) => T | void) => void] {
const { store, node } = createProjectionNextInternal(fn, seed, options);
Expand Down
10 changes: 5 additions & 5 deletions packages/signals/tests/store/createProjection.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -518,7 +518,7 @@ describe("projection over a store — chained backing (#2941)", () => {
proj = createProjection(() => {
derive();
return store;
}, {}) as { a: number };
}, {} as any) as { a: number };
createEffect(
() => proj.a,
v => {
Expand Down Expand Up @@ -547,7 +547,7 @@ describe("projection over a store — chained backing (#2941)", () => {
const dispose = createRoot(dispose => {
const [store, set] = createStore({ top: "t1", nest: { deep: "d1" } });
setStore = set;
const proj = createProjection(() => store, {}) as typeof store;
const proj = createProjection(() => store, {} as any) as typeof store;
createEffect(
() => proj.top,
v => {
Expand Down Expand Up @@ -587,7 +587,7 @@ describe("projection over a store — chained backing (#2941)", () => {
const proj = createProjection(() => {
const m = mode();
return m === "a" ? a : m === "b" ? b : { v: "plain" };
}, {}) as { v: string };
}, {} as any) as { v: string };
createEffect(
() => proj.v,
v => {
Expand Down Expand Up @@ -676,7 +676,7 @@ describe("projection over a store — chained backing (#2941)", () => {
const dispose = createRoot(dispose => {
const [store, set] = createStore({ a: 1, nest: { b: 2 } });
setStore = set;
proj = createProjection(() => store, {});
proj = createProjection(() => store, {} as any);
return dispose;
});
flush();
Expand All @@ -697,7 +697,7 @@ describe("projection over a store — chained backing (#2941)", () => {
const dispose = createRoot(dispose => {
const [store, set] = createStore<Record<string, number>>({ a: 1 });
setStore = set;
const proj = createProjection(() => store, {}) as Record<string, number>;
const proj = createProjection(() => store, {} as any) as Record<string, number>;
createEffect(
() => Object.keys(proj).join(","),
v => {
Expand Down
56 changes: 24 additions & 32 deletions packages/signals/tests/store/store.type-tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,24 +32,29 @@ import {
store.count satisfies number;
}

// ── createStore (projection) — partial seed ───────────────────────────
// ── projection seeds must be complete ────────────────────────────────

{
const [store] = createStore(() => ({ foo: true }), {});
store.foo satisfies boolean;
}
type UserState = { user: { name: string }; ready: boolean };

{
const [store] = createStore(() => ({ a: 1, b: "hello" }), {});
store.a satisfies number;
store.b satisfies string;
}
// @ts-expect-error An inferred seed cannot omit required properties.
createStore(() => ({ user: { name: "Ada" }, ready: true }), { ready: false });

{
const [store] = createStore(() => ({ a: 1, b: "hello" }), { a: 0 });
store.a satisfies number;
store.b satisfies string;
}
// @ts-expect-error An explicit type argument cannot opt back into a partial seed.
createStore<UserState>(() => ({ user: { name: "Ada" }, ready: true }), {});

// ── callable store roots are rejected ────────────────────────────────

type CallableState = (() => void) & { count: number };
const callableState = Object.assign(() => {}, { count: 0 });

// @ts-expect-error A projection draft cannot represent a callable root.
createStore<CallableState>(() => callableState, callableState);

// @ts-expect-error A projection draft cannot represent a callable root.
createProjection<CallableState>(() => callableState, callableState);

// @ts-expect-error A projection draft cannot represent a callable root.
createOptimisticStore<CallableState>(() => callableState, callableState);

// ── createProjection — mutation only (void return, T from seed) ───────

Expand All @@ -75,17 +80,8 @@ import {
store.active satisfies boolean;
}

// ── createProjection — partial seed ───────────────────────────────────

{
const store = createProjection(() => ({ foo: true }), {});
store.foo satisfies boolean;
}

{
const store = createProjection(() => ({ nested: { x: 1 } }), {});
store.nested.x satisfies number;
}
// @ts-expect-error createProjection also requires a complete seed.
createProjection(() => ({ user: { name: "Ada" }, ready: true }), { ready: false });

// ── createProjection — empty array seed ───────────────────────────────

Expand Down Expand Up @@ -113,12 +109,8 @@ import {
proj.count satisfies number;
}

// ── createOptimisticStore (projection) — partial seed ─────────────────

{
const [store] = createOptimisticStore(() => ({ foo: true }), {});
store.foo satisfies boolean;
}
// @ts-expect-error createOptimisticStore also requires a complete seed.
createOptimisticStore(() => ({ user: { name: "Ada" }, ready: true }), { ready: false });

// ── createOptimisticStore (projection) — seed matches return type ─────

Expand Down
2 changes: 1 addition & 1 deletion packages/solid/src/client/hydration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1592,7 +1592,7 @@ export const createOptimistic: {
*/
export const createProjection: <T extends object = {}>(
fn: (draft: T) => void | T | Promise<void | T> | AsyncIterable<void | T>,
initialValue: Partial<T> | Store<NoFn<T>>,
initialValue: NoFn<T> | Store<NoFn<T>>,
options?: HydrationProjectionOptions
) => Refreshable<Store<T>> = ((...args: any[]) => {
// `hydrating` can only be true once enableHydration() installed the
Expand Down
15 changes: 8 additions & 7 deletions packages/solid/src/server/signals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,7 @@ type ServerMemoOptions<T> = MemoOptions<T> & {
};
type ServerSignalOptions<T> = SignalOptions<T>;
type ServerStoreOptions = ServerSsrOptions;
type NoFn<T> = T extends Function ? never : T;

/**
* The pending source for BARE `ssrSource: "client"` (no declared commit #0):
Expand Down Expand Up @@ -1778,12 +1779,12 @@ function setProperty(state: any, property: PropertyKey, value: any) {
}

export function createStore<T extends object>(
store: T | Store<T>,
store: NoFn<T> | Store<NoFn<T>>,
options?: { name?: string; shallow?: boolean }
): [get: Store<T>, set: StoreSetter<T>];
export function createStore<T extends object>(
fn: (store: T) => void | T | Promise<void | T>,
store: Partial<T> | Store<T>,
store: NoFn<T> | Store<NoFn<T>>,
options?: ServerStoreOptions & { name?: string; shallow?: boolean }
): [get: Store<T>, set: StoreSetter<T>];
export function createStore<T extends object>(
Expand All @@ -1798,7 +1799,7 @@ export function createStore<T extends object>(
// The impl signature stays loose; the public overload above enforces the
// client/seedLoadingValue pairing, and createProjection re-checks at
// runtime.
const store = createProjection(first as any, second as T, options as any);
const store = createProjection(first as any, second as NoFn<T>, options as any);
return [store as Store<T>, storeSetter(store as T)];
}
const state = first as T;
Expand All @@ -1823,12 +1824,12 @@ function storeSetter<T extends object>(state: T): StoreSetter<T> {
}

export function createOptimisticStore<T extends object>(
store: T | Store<T>,
store: NoFn<T> | Store<NoFn<T>>,
options?: { name?: string; shallow?: boolean }
): [get: Store<T>, set: StoreSetter<T>];
export function createOptimisticStore<T extends object>(
fn: (store: T) => void | T | Promise<void | T>,
store: Partial<T> | Store<T>,
store: NoFn<T> | Store<NoFn<T>>,
options?: ServerStoreOptions & { name?: string; shallow?: boolean }
): [get: Store<T>, set: StoreSetter<T>];
export function createOptimisticStore<T extends object>(
Expand Down Expand Up @@ -1969,7 +1970,7 @@ function replaceState<T extends object>(target: T, next: T): void {

export function createProjection<T extends object>(
fn: (draft: T) => void | T | Promise<void | T> | AsyncIterable<void | T>,
initialValue: Partial<T> | Store<T>,
initialValue: NoFn<T> | Store<NoFn<T>>,
options?: ServerStoreOptions
): Store<T> {
const ctx = sharedConfig.context;
Expand Down Expand Up @@ -1999,7 +2000,7 @@ export function createProjection<T extends object>(
if (slots) slots[slotId!] = proxy;
return proxy;
};
const [state] = createStore(initialValue as T);
const [state] = createStore(initialValue as NoFn<T>);

if (options?.ssrSource === "client") {
// seedLoadingValue = declared commit #0: the seed renders. Bare = the
Expand Down
2 changes: 1 addition & 1 deletion packages/solid/test/server/ssr-async.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1593,7 +1593,7 @@ describe("Stream Blocking / deferStream", () => {

createRoot(
() => {
createProjection(() => d.promise, {} as { name?: string });
createProjection(() => d.promise, {} as any);
},
{ id: "t" }
);
Expand Down
Loading