From 56a6753b88e42ebba50c53bdd868e8dbadbd37ba Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Tue, 7 Jul 2026 16:48:35 +0200 Subject: [PATCH 1/6] breaking: make `page.url` immutable on a type level It's not reactive, it should be treated as readonly, and the types now reflect that. closes #12870 --- .changeset/easy-pianos-float.md | 5 +++++ packages/kit/src/exports/public.d.ts | 6 +++++- packages/kit/src/runtime/client/client.js | 2 +- .../apps/basics/src/routes/load/mutated-url/+page.svelte | 2 ++ .../kit/test/apps/basics/src/routes/query/echo/utils.js | 2 +- .../kit/test/apps/basics/src/routes/state/url/+page.svelte | 2 ++ packages/kit/test/apps/basics/src/routes/xss/query/utils.js | 2 +- packages/kit/types/index.d.ts | 6 +++++- 8 files changed, 22 insertions(+), 5 deletions(-) create mode 100644 .changeset/easy-pianos-float.md diff --git a/.changeset/easy-pianos-float.md b/.changeset/easy-pianos-float.md new file mode 100644 index 000000000000..b755e97f0680 --- /dev/null +++ b/.changeset/easy-pianos-float.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': major +--- + +breaking: make `page.url` immutable on a type level diff --git a/packages/kit/src/exports/public.d.ts b/packages/kit/src/exports/public.d.ts index 4956a228370e..b0ac3b539823 100644 --- a/packages/kit/src/exports/public.d.ts +++ b/packages/kit/src/exports/public.d.ts @@ -1454,7 +1454,11 @@ export interface Page< /** * The URL of the current page. */ - url: URL & { pathname: ResolvedPathname }; + url: Readonly< + Omit & { pathname: ResolvedPathname } & { + searchParams: Omit; + } + >; /** * The parameters of the current page - e.g. for a route like `/blog/[slug]`, a `{ slug: string }` object. */ diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js index cfdc94c08a61..d1bf99afb584 100644 --- a/packages/kit/src/runtime/client/client.js +++ b/packages/kit/src/runtime/client/client.js @@ -2649,7 +2649,7 @@ export async function applyAction(result) { root.$set({ form: result.data }); if (result.type === 'success') { - reset_focus(page.url); + reset_focus(/** @type {URL} */ (page.url)); } } } diff --git a/packages/kit/test/apps/basics/src/routes/load/mutated-url/+page.svelte b/packages/kit/test/apps/basics/src/routes/load/mutated-url/+page.svelte index 1c71caabb32e..2a50cf9e4937 100644 --- a/packages/kit/test/apps/basics/src/routes/load/mutated-url/+page.svelte +++ b/packages/kit/test/apps/basics/src/routes/load/mutated-url/+page.svelte @@ -6,7 +6,9 @@ let { data } = $props(); function update_q() { + // @ts-expect-error set is not in the types; we wanna test here that we guard against mutation in goto, too page.url.searchParams.set('q', 'updated'); + // @ts-expect-error TODO should we relax goto to accept page.url? goto(page.url); } diff --git a/packages/kit/test/apps/basics/src/routes/query/echo/utils.js b/packages/kit/test/apps/basics/src/routes/query/echo/utils.js index 97023e74857f..cfb79f9286e6 100644 --- a/packages/kit/test/apps/basics/src/routes/query/echo/utils.js +++ b/packages/kit/test/apps/basics/src/routes/query/echo/utils.js @@ -1,6 +1,6 @@ /** @typedef {Record} Query */ -/** @param {URLSearchParams} query */ +/** @param {Pick} query */ export function to_pojo(query) { /** @type {Query}*/ const values = {}; diff --git a/packages/kit/test/apps/basics/src/routes/state/url/+page.svelte b/packages/kit/test/apps/basics/src/routes/state/url/+page.svelte index c8d9d6ec90b5..ce968071a91f 100644 --- a/packages/kit/test/apps/basics/src/routes/state/url/+page.svelte +++ b/packages/kit/test/apps/basics/src/routes/state/url/+page.svelte @@ -8,7 +8,9 @@ diff --git a/packages/kit/test/apps/basics/src/routes/xss/query/utils.js b/packages/kit/test/apps/basics/src/routes/xss/query/utils.js index 97023e74857f..cfb79f9286e6 100644 --- a/packages/kit/test/apps/basics/src/routes/xss/query/utils.js +++ b/packages/kit/test/apps/basics/src/routes/xss/query/utils.js @@ -1,6 +1,6 @@ /** @typedef {Record} Query */ -/** @param {URLSearchParams} query */ +/** @param {Pick} query */ export function to_pojo(query) { /** @type {Query}*/ const values = {}; diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts index f3d515b856ae..a71020965f70 100644 --- a/packages/kit/types/index.d.ts +++ b/packages/kit/types/index.d.ts @@ -1427,7 +1427,11 @@ declare module '@sveltejs/kit' { /** * The URL of the current page. */ - url: URL & { pathname: ResolvedPathname }; + url: Readonly< + Omit & { pathname: ResolvedPathname } & { + searchParams: Omit; + } + >; /** * The parameters of the current page - e.g. for a route like `/blog/[slug]`, a `{ slug: string }` object. */ From f6f65e6432efd171a0b3540c53e8ef19a6d85b4b Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Tue, 7 Jul 2026 21:51:39 +0200 Subject: [PATCH 2/6] ReadonlyURLSearchParams --- packages/kit/src/exports/public.d.ts | 4 +++- packages/kit/test/apps/basics/src/routes/query/echo/utils.js | 2 +- packages/kit/test/apps/basics/src/routes/xss/query/utils.js | 2 +- packages/kit/types/index.d.ts | 4 +++- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/kit/src/exports/public.d.ts b/packages/kit/src/exports/public.d.ts index b0ac3b539823..9be8bde72125 100644 --- a/packages/kit/src/exports/public.d.ts +++ b/packages/kit/src/exports/public.d.ts @@ -1456,7 +1456,7 @@ export interface Page< */ url: Readonly< Omit & { pathname: ResolvedPathname } & { - searchParams: Omit; + searchParams: ReadonlyURLSearchParams; } >; /** @@ -1999,6 +1999,8 @@ export interface Snapshot { restore: (snapshot: T) => void; } +export type ReadonlyURLSearchParams = Omit; + // If T is unknown or has an index signature, the types below will recurse indefinitely and create giant unions that TS can't handle type WillRecurseIndefinitely = unknown extends T ? true : string extends keyof T ? true : false; diff --git a/packages/kit/test/apps/basics/src/routes/query/echo/utils.js b/packages/kit/test/apps/basics/src/routes/query/echo/utils.js index cfb79f9286e6..fc717cb747ae 100644 --- a/packages/kit/test/apps/basics/src/routes/query/echo/utils.js +++ b/packages/kit/test/apps/basics/src/routes/query/echo/utils.js @@ -1,6 +1,6 @@ /** @typedef {Record} Query */ -/** @param {Pick} query */ +/** @param {import("@sveltejs/kit").ReadonlyURLSearchParams} query */ export function to_pojo(query) { /** @type {Query}*/ const values = {}; diff --git a/packages/kit/test/apps/basics/src/routes/xss/query/utils.js b/packages/kit/test/apps/basics/src/routes/xss/query/utils.js index cfb79f9286e6..fc717cb747ae 100644 --- a/packages/kit/test/apps/basics/src/routes/xss/query/utils.js +++ b/packages/kit/test/apps/basics/src/routes/xss/query/utils.js @@ -1,6 +1,6 @@ /** @typedef {Record} Query */ -/** @param {Pick} query */ +/** @param {import("@sveltejs/kit").ReadonlyURLSearchParams} query */ export function to_pojo(query) { /** @type {Query}*/ const values = {}; diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts index a71020965f70..81c2ab9a9beb 100644 --- a/packages/kit/types/index.d.ts +++ b/packages/kit/types/index.d.ts @@ -1429,7 +1429,7 @@ declare module '@sveltejs/kit' { */ url: Readonly< Omit & { pathname: ResolvedPathname } & { - searchParams: Omit; + searchParams: ReadonlyURLSearchParams; } >; /** @@ -1970,6 +1970,8 @@ declare module '@sveltejs/kit' { restore: (snapshot: T) => void; } + export type ReadonlyURLSearchParams = Omit; + // If T is unknown or has an index signature, the types below will recurse indefinitely and create giant unions that TS can't handle type WillRecurseIndefinitely = unknown extends T ? true : string extends keyof T ? true : false; From 67ae14080148652d363313035a48501adf7afeea Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Wed, 8 Jul 2026 23:17:06 +0200 Subject: [PATCH 3/6] ReadonlyURL --- packages/kit/src/exports/public.d.ts | 12 +++++++----- packages/kit/types/index.d.ts | 12 +++++++----- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/packages/kit/src/exports/public.d.ts b/packages/kit/src/exports/public.d.ts index 9be8bde72125..54abe95cdf20 100644 --- a/packages/kit/src/exports/public.d.ts +++ b/packages/kit/src/exports/public.d.ts @@ -1454,11 +1454,7 @@ export interface Page< /** * The URL of the current page. */ - url: Readonly< - Omit & { pathname: ResolvedPathname } & { - searchParams: ReadonlyURLSearchParams; - } - >; + url: ReadonlyURL & { pathname: ResolvedPathname | (string & {}) }; /** * The parameters of the current page - e.g. for a route like `/blog/[slug]`, a `{ slug: string }` object. */ @@ -2001,6 +1997,12 @@ export interface Snapshot { export type ReadonlyURLSearchParams = Omit; +export type ReadonlyURL = Readonly< + Omit & { + searchParams: ReadonlyURLSearchParams; + } +>; + // If T is unknown or has an index signature, the types below will recurse indefinitely and create giant unions that TS can't handle type WillRecurseIndefinitely = unknown extends T ? true : string extends keyof T ? true : false; diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts index 81c2ab9a9beb..3cd30678bfdf 100644 --- a/packages/kit/types/index.d.ts +++ b/packages/kit/types/index.d.ts @@ -1427,11 +1427,7 @@ declare module '@sveltejs/kit' { /** * The URL of the current page. */ - url: Readonly< - Omit & { pathname: ResolvedPathname } & { - searchParams: ReadonlyURLSearchParams; - } - >; + url: ReadonlyURL & { pathname: ResolvedPathname | (string & {}) }; /** * The parameters of the current page - e.g. for a route like `/blog/[slug]`, a `{ slug: string }` object. */ @@ -1972,6 +1968,12 @@ declare module '@sveltejs/kit' { export type ReadonlyURLSearchParams = Omit; + export type ReadonlyURL = Readonly< + Omit & { + searchParams: ReadonlyURLSearchParams; + } + >; + // If T is unknown or has an index signature, the types below will recurse indefinitely and create giant unions that TS can't handle type WillRecurseIndefinitely = unknown extends T ? true : string extends keyof T ? true : false; From 3003d534ada6b94446962058ed0dfcc2e48e9074 Mon Sep 17 00:00:00 2001 From: "vercel[bot]" <35613825+vercel[bot]@users.noreply.github.com> Date: Wed, 8 Jul 2026 21:22:35 +0000 Subject: [PATCH 4/6] Fix: `page.url.pathname` is mutable at the type level, defeating the PR goal of making `page.url` immutable. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit fixes the issue reported at packages/kit/src/exports/public.d.ts:1457 ## Bug In `packages/kit/src/exports/public.d.ts` (line 1457) and its generated twin `packages/kit/types/index.d.ts` (line 1430), the `Page.url` type was declared as: ```ts url: ReadonlyURL & { pathname: ResolvedPathname | (string & {}) }; ``` `ReadonlyURL` is defined as: ```ts export type ReadonlyURL = Readonly< Omit & { searchParams: ReadonlyURLSearchParams } >; ``` so `pathname` coming from `ReadonlyURL` is correctly `readonly`. However, the intersection adds a second constituent `{ pathname: ... }` where `pathname` is **writable**. In TypeScript, a property in an intersection type is only `readonly` if it is `readonly` in *every* constituent. Because the override constituent lacks the modifier, the effective `pathname` becomes assignable again: ```ts declare const a: ReadonlyURL & { pathname: ResolvedPathname | (string & {}) }; a.pathname = 'foo'; // NO ERROR — regression ``` This defeats the PR's stated goal of making `page.url` immutable on a type level (`href` and `searchParams.set` remained correctly blocked, but `pathname` did not). ## Fix Add the `readonly` modifier to the overriding property in both files: ```ts url: ReadonlyURL & { readonly pathname: ResolvedPathname | (string & {}) }; ``` Now `pathname` is `readonly` in both intersection constituents, so assignment is correctly rejected. Both the source (`public.d.ts`) and the generated (`types/index.d.ts`) declarations were updated to stay in sync. Co-authored-by: Vercel Co-authored-by: Rich-Harris --- packages/kit/src/exports/public.d.ts | 2 +- packages/kit/types/index.d.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/kit/src/exports/public.d.ts b/packages/kit/src/exports/public.d.ts index 54abe95cdf20..9d3a7610b9c0 100644 --- a/packages/kit/src/exports/public.d.ts +++ b/packages/kit/src/exports/public.d.ts @@ -1454,7 +1454,7 @@ export interface Page< /** * The URL of the current page. */ - url: ReadonlyURL & { pathname: ResolvedPathname | (string & {}) }; + url: ReadonlyURL & { readonly pathname: ResolvedPathname | (string & {}) }; /** * The parameters of the current page - e.g. for a route like `/blog/[slug]`, a `{ slug: string }` object. */ diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts index 3cd30678bfdf..a0a04a530ec3 100644 --- a/packages/kit/types/index.d.ts +++ b/packages/kit/types/index.d.ts @@ -1427,7 +1427,7 @@ declare module '@sveltejs/kit' { /** * The URL of the current page. */ - url: ReadonlyURL & { pathname: ResolvedPathname | (string & {}) }; + url: ReadonlyURL & { readonly pathname: ResolvedPathname | (string & {}) }; /** * The parameters of the current page - e.g. for a route like `/blog/[slug]`, a `{ slug: string }` object. */ From 7ac2d3ac6f8b4a5f9a5348b29b92120ae4d1a5f6 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Wed, 8 Jul 2026 23:26:34 +0200 Subject: [PATCH 5/6] Apply suggestions from code review Co-authored-by: Rich Harris --- .../test/apps/basics/src/routes/load/mutated-url/+page.svelte | 2 +- packages/kit/test/apps/basics/src/routes/state/url/+page.svelte | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/kit/test/apps/basics/src/routes/load/mutated-url/+page.svelte b/packages/kit/test/apps/basics/src/routes/load/mutated-url/+page.svelte index 2a50cf9e4937..40e485ddde9b 100644 --- a/packages/kit/test/apps/basics/src/routes/load/mutated-url/+page.svelte +++ b/packages/kit/test/apps/basics/src/routes/load/mutated-url/+page.svelte @@ -8,7 +8,7 @@ function update_q() { // @ts-expect-error set is not in the types; we wanna test here that we guard against mutation in goto, too page.url.searchParams.set('q', 'updated'); - // @ts-expect-error TODO should we relax goto to accept page.url? + // @ts-expect-error goto(page.url); } diff --git a/packages/kit/test/apps/basics/src/routes/state/url/+page.svelte b/packages/kit/test/apps/basics/src/routes/state/url/+page.svelte index ce968071a91f..2aa614849bfd 100644 --- a/packages/kit/test/apps/basics/src/routes/state/url/+page.svelte +++ b/packages/kit/test/apps/basics/src/routes/state/url/+page.svelte @@ -10,7 +10,7 @@ onclick={() => { // @ts-expect-error set is not in the types; we wanna test here that we guard against mutation in goto, too page.url.searchParams.set('q', 'test'); - // @ts-expect-error TODO should we relax goto to accept page.url? + // @ts-expect-error goto(page.url); }}>test From 943255a89c6b2d60c5d2f5b3c798df04e70d677e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 8 Jul 2026 22:05:34 +0000 Subject: [PATCH 6/6] chore: autofix lint --- .../test/apps/basics/src/routes/load/mutated-url/+page.svelte | 2 +- packages/kit/test/apps/basics/src/routes/state/url/+page.svelte | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/kit/test/apps/basics/src/routes/load/mutated-url/+page.svelte b/packages/kit/test/apps/basics/src/routes/load/mutated-url/+page.svelte index 40e485ddde9b..41f6b2cfc41a 100644 --- a/packages/kit/test/apps/basics/src/routes/load/mutated-url/+page.svelte +++ b/packages/kit/test/apps/basics/src/routes/load/mutated-url/+page.svelte @@ -8,7 +8,7 @@ function update_q() { // @ts-expect-error set is not in the types; we wanna test here that we guard against mutation in goto, too page.url.searchParams.set('q', 'updated'); - // @ts-expect-error + // @ts-expect-error goto(page.url); } diff --git a/packages/kit/test/apps/basics/src/routes/state/url/+page.svelte b/packages/kit/test/apps/basics/src/routes/state/url/+page.svelte index 2aa614849bfd..d835bb91a268 100644 --- a/packages/kit/test/apps/basics/src/routes/state/url/+page.svelte +++ b/packages/kit/test/apps/basics/src/routes/state/url/+page.svelte @@ -10,7 +10,7 @@ onclick={() => { // @ts-expect-error set is not in the types; we wanna test here that we guard against mutation in goto, too page.url.searchParams.set('q', 'test'); - // @ts-expect-error + // @ts-expect-error goto(page.url); }}>test