From d1190864f9fa26ad27856aa2100e3ee68c8c9457 Mon Sep 17 00:00:00 2001 From: Jungzl <13jungzl@gmail.com> Date: Tue, 14 Apr 2026 14:20:17 +0800 Subject: [PATCH 1/5] fix: add regression test for tuple type preservation in snapshot --- packages/svelte/tests/types/state.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 packages/svelte/tests/types/state.ts diff --git a/packages/svelte/tests/types/state.ts b/packages/svelte/tests/types/state.ts new file mode 100644 index 0000000000..6237f32075 --- /dev/null +++ b/packages/svelte/tests/types/state.ts @@ -0,0 +1,15 @@ +import type { ClassValue } from 'svelte/elements'; + +// Regression test for #17117: this should not trigger deep/infinite type instantiation +const cls = $state.raw([]); +const cls_snap: ReturnType> = + $state.snapshot(cls); + +type SnapshotTuple = ReturnType>; + +const tuple_ok: SnapshotTuple = [1, 'a']; +tuple_ok[0] === 1; +tuple_ok[1] === 'a'; + +// @ts-expect-error tuple element type should be preserved +const tuple_wrong: SnapshotTuple = [1, 2]; From 936d53761108a5afd656e6c32272e80b85c5ca4f Mon Sep 17 00:00:00 2001 From: Jungzl <13jungzl@gmail.com> Date: Tue, 14 Apr 2026 14:37:12 +0800 Subject: [PATCH 2/5] chore: update import for ClassValue type from 'clsx' --- packages/svelte/tests/types/state.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/svelte/tests/types/state.ts b/packages/svelte/tests/types/state.ts index 6237f32075..989a497b59 100644 --- a/packages/svelte/tests/types/state.ts +++ b/packages/svelte/tests/types/state.ts @@ -1,4 +1,4 @@ -import type { ClassValue } from 'svelte/elements'; +import type { ClassValue } from 'clsx'; // Regression test for #17117: this should not trigger deep/infinite type instantiation const cls = $state.raw([]); From 41dacc1dbf0c94b7cfadf788518e6a58f96c3a36 Mon Sep 17 00:00:00 2001 From: Jungzl <13jungzl@gmail.com> Date: Tue, 14 Apr 2026 14:45:43 +0800 Subject: [PATCH 3/5] fix: enhance snapshot type handling for array structures in $state --- packages/svelte/src/ambient.d.ts | 8 +++++++- packages/svelte/types/index.d.ts | 32 +++++++++++++++++++------------- 2 files changed, 26 insertions(+), 14 deletions(-) diff --git a/packages/svelte/src/ambient.d.ts b/packages/svelte/src/ambient.d.ts index 159a568477..9d37116e56 100644 --- a/packages/svelte/src/ambient.d.ts +++ b/packages/svelte/src/ambient.d.ts @@ -86,7 +86,13 @@ declare namespace $state { : T extends { toJSON(): infer R } ? R : T extends readonly unknown[] - ? { [K in keyof T]: Snapshot } + ? number extends T['length'] + ? T extends Array + ? Array> + : T extends ReadonlyArray + ? ReadonlyArray> + : never + : { [K in keyof T]: Snapshot } : T extends Array ? Array> : T extends object diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index 019baf45dd..f229ed48d3 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -2203,10 +2203,10 @@ declare module 'svelte/motion' { * const tween = Tween.of(() => number); * * ``` - * + * */ static of(fn: () => U, options?: TweenOptions | undefined): Tween; - + constructor(value: T, options?: TweenOptions); /** * Sets `tween.target` to `value` and returns a `Promise` that resolves if and when `tween.current` catches up to it. @@ -2257,7 +2257,7 @@ declare module 'svelte/reactivity' { * ``` */ export class SvelteDate extends Date { - + constructor(...params: any[]); #private; } @@ -2293,12 +2293,12 @@ declare module 'svelte/reactivity' { * {#if monkeys.has('🙊')}

speak no evil

{/if} * ``` * - * + * */ export class SvelteSet extends Set { - + constructor(value?: Iterable | null | undefined); - + add(value: T): this; #private; } @@ -2344,12 +2344,12 @@ declare module 'svelte/reactivity' { * {/if} * ``` * - * + * */ export class SvelteMap extends Map { - + constructor(value?: Iterable | null | undefined); - + set(key: K, value: V): this; #private; } @@ -2412,7 +2412,7 @@ declare module 'svelte/reactivity' { * ``` */ export class SvelteURLSearchParams extends URLSearchParams { - + [REPLACE](params: URLSearchParams): void; #private; } @@ -2488,7 +2488,7 @@ declare module 'svelte/reactivity' { */ export function createSubscriber(start: (update: () => void) => (() => void) | void): () => void; class ReactiveValue { - + constructor(fn: () => T, onsubscribe: (update: () => void) => void); get current(): T; #private; @@ -2553,7 +2553,7 @@ declare module 'svelte/reactivity/window' { get current(): number | undefined; }; class ReactiveValue { - + constructor(fn: () => T, onsubscribe: (update: () => void) => void); get current(): T; #private; @@ -3284,7 +3284,13 @@ declare namespace $state { : T extends { toJSON(): infer R } ? R : T extends readonly unknown[] - ? { [K in keyof T]: Snapshot } + ? number extends T['length'] + ? T extends Array + ? Array> + : T extends ReadonlyArray + ? ReadonlyArray> + : never + : { [K in keyof T]: Snapshot } : T extends Array ? Array> : T extends object From 550e0a02a5fa6cba7c5f41ca32cf31cd15adfe71 Mon Sep 17 00:00:00 2001 From: Jungzl <13jungzl@gmail.com> Date: Tue, 14 Apr 2026 14:54:53 +0800 Subject: [PATCH 4/5] chore: tweak --- packages/svelte/types/index.d.ts | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index f229ed48d3..bd36f6f0e1 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -2203,10 +2203,10 @@ declare module 'svelte/motion' { * const tween = Tween.of(() => number); * * ``` - * + * */ static of(fn: () => U, options?: TweenOptions | undefined): Tween; - + constructor(value: T, options?: TweenOptions); /** * Sets `tween.target` to `value` and returns a `Promise` that resolves if and when `tween.current` catches up to it. @@ -2257,7 +2257,7 @@ declare module 'svelte/reactivity' { * ``` */ export class SvelteDate extends Date { - + constructor(...params: any[]); #private; } @@ -2293,12 +2293,12 @@ declare module 'svelte/reactivity' { * {#if monkeys.has('🙊')}

speak no evil

{/if} * ``` * - * + * */ export class SvelteSet extends Set { - + constructor(value?: Iterable | null | undefined); - + add(value: T): this; #private; } @@ -2344,12 +2344,12 @@ declare module 'svelte/reactivity' { * {/if} * ``` * - * + * */ export class SvelteMap extends Map { - + constructor(value?: Iterable | null | undefined); - + set(key: K, value: V): this; #private; } @@ -2412,7 +2412,7 @@ declare module 'svelte/reactivity' { * ``` */ export class SvelteURLSearchParams extends URLSearchParams { - + [REPLACE](params: URLSearchParams): void; #private; } @@ -2488,7 +2488,7 @@ declare module 'svelte/reactivity' { */ export function createSubscriber(start: (update: () => void) => (() => void) | void): () => void; class ReactiveValue { - + constructor(fn: () => T, onsubscribe: (update: () => void) => void); get current(): T; #private; @@ -2553,7 +2553,7 @@ declare module 'svelte/reactivity/window' { get current(): number | undefined; }; class ReactiveValue { - + constructor(fn: () => T, onsubscribe: (update: () => void) => void); get current(): T; #private; From 12a605188414df84f78ac6f2e5555795595b139b Mon Sep 17 00:00:00 2001 From: Jungzl <13jungzl@gmail.com> Date: Tue, 14 Apr 2026 15:04:57 +0800 Subject: [PATCH 5/5] add changeset --- .changeset/twenty-bears-fly.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/twenty-bears-fly.md diff --git a/.changeset/twenty-bears-fly.md b/.changeset/twenty-bears-fly.md new file mode 100644 index 0000000000..e32299c2d3 --- /dev/null +++ b/.changeset/twenty-bears-fly.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: enhance snapshot type handling for array structures in $state.snapshot