From 9f333a8231b1f5f742575a24e5560950830be745 Mon Sep 17 00:00:00 2001 From: EchoCrow <5488190+echocrow@users.noreply.github.com> Date: Sat, 23 May 2026 00:11:10 +0200 Subject: [PATCH 1/6] silence needless eslint errors --- packages/kit/test/types/remote.test.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/kit/test/types/remote.test.ts b/packages/kit/test/types/remote.test.ts index 8c44aa4e3d74..b2d3de09681d 100644 --- a/packages/kit/test/types/remote.test.ts +++ b/packages/kit/test/types/remote.test.ts @@ -10,6 +10,9 @@ import { invalid } from '@sveltejs/kit'; +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-floating-promises */ + const schema: StandardSchemaV1 = null as any; const schema2: StandardSchemaV1 = null as any; const schema3: StandardSchemaV1 = null as any; From 0a8f4ba5d18ee12c3b45951172211442cdd78149 Mon Sep 17 00:00:00 2001 From: EchoCrow <5488190+echocrow@users.noreply.github.com> Date: Sat, 23 May 2026 00:22:34 +0200 Subject: [PATCH 2/6] relocate test line --- packages/kit/test/types/remote.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/kit/test/types/remote.test.ts b/packages/kit/test/types/remote.test.ts index b2d3de09681d..8d259da7fbb4 100644 --- a/packages/kit/test/types/remote.test.ts +++ b/packages/kit/test/types/remote.test.ts @@ -518,6 +518,8 @@ function form_tests() { f6.fields.array[0].array.value(); // @ts-expect-error f6.fields.array[0].array.as('text'); + // @ts-expect-error + f6.input!['array[0].prop'] = 123; // any const f7 = form(null as any, (data, issue) => { @@ -543,8 +545,6 @@ function form_tests() { f8.fields.allIssues(); // @ts-expect-error f8.fields.x; - // @ts-expect-error - f6.input!['array[0].prop'] = 123; // schema with optional array fields (e.g. Zod's `.default([])` produces an input // type where the property is optional, so its value type is `string[] | undefined`). From 3dd0e17c26285f33cc3bd24cbe7f22cf59511b55 Mon Sep 17 00:00:00 2001 From: EchoCrow <5488190+echocrow@users.noreply.github.com> Date: Sat, 23 May 2026 00:29:20 +0200 Subject: [PATCH 3/6] add failing tests --- packages/kit/test/types/remote.test.ts | 37 ++++++++++++++++++++++++++ packages/kit/test/types/tsconfig.json | 4 ++- 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/packages/kit/test/types/remote.test.ts b/packages/kit/test/types/remote.test.ts index 8d259da7fbb4..780ec6717769 100644 --- a/packages/kit/test/types/remote.test.ts +++ b/packages/kit/test/types/remote.test.ts @@ -61,6 +61,23 @@ function query_tests() { } void query_with_optional_arg(); + async function query_with_optional_undefined_arg() { + const q = query( + null as any as StandardSchemaV1<{ a?: string | undefined }>, + () => 'Hello world' + ); + // @ts-expect-error + void q(); + void q({}); + void q({ a: 'hi' }); + void q({ a: undefined }); + // @ts-expect-error + void q({ a: null }); + // @ts-expect-error + void q(1); + } + void query_with_optional_undefined_arg(); + async function query_unsafe() { const q = query('unchecked', (a: number) => a); const result: number = await q(1); @@ -581,6 +598,26 @@ function form_tests() { // @ts-expect-error f_optional_arrays.fields.files.as('text'); + // schema with optional & value-undefined fields. (e.g. Valibot's `.optional()` + // produces an input type that accepts `undefined` as value, which under + // `exactOptionalPropertyTypes` is treated distinctly from an omitted property.) + const f_optional_undefined_prop = form( + null as any as StandardSchemaV1<{ + strings?: string[] | undefined; + }>, + (data) => { + data.strings?.[0] === 'a'; + return { success: true }; + } + ); + // `.as()` should be available on optional|undefined fields + f_optional_undefined_prop.fields.strings.as('checkbox', 'value'); + f_optional_undefined_prop.fields.strings.as('select multiple'); + // indexed access gives back a typed field + f_optional_undefined_prop.fields.strings[0].as('text'); + // @ts-expect-error + f_optional_undefined_prop.fields.strings.as('number'); + // doesn't use data const f9 = form(() => Promise.resolve({ success: true })); f9.result?.success === true; diff --git a/packages/kit/test/types/tsconfig.json b/packages/kit/test/types/tsconfig.json index 7675e28d941c..2e8a2b2182fb 100644 --- a/packages/kit/test/types/tsconfig.json +++ b/packages/kit/test/types/tsconfig.json @@ -1,7 +1,9 @@ { "extends": "../../tsconfig.json", "compilerOptions": { - "noEmit": true + "exactOptionalPropertyTypes": true, + "noEmit": true, + "skipLibCheck": true }, "include": ["**/*.test.ts", "../../src/types/*.d.ts"] } From af9bbd8f9030a77a9eb220b8622d70450121f106 Mon Sep 17 00:00:00 2001 From: EchoCrow <5488190+echocrow@users.noreply.github.com> Date: Sat, 23 May 2026 01:17:12 +0200 Subject: [PATCH 4/6] pass failing tests due to exactOptionalPropertyTypes --- packages/kit/src/runtime/client/client.js | 39 +++++++++++++--------- packages/kit/src/runtime/client/types.d.ts | 4 +-- packages/kit/src/types/internal.d.ts | 4 +-- 3 files changed, 27 insertions(+), 20 deletions(-) diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js index a5afd3c03356..1c52f20aa2b4 100644 --- a/packages/kit/src/runtime/client/client.js +++ b/packages/kit/src/runtime/client/client.js @@ -514,7 +514,14 @@ function persist_state() { /** * @param {string | URL} url - * @param {{ replaceState?: boolean; noScroll?: boolean; keepFocus?: boolean; invalidateAll?: boolean; invalidate?: Array boolean)>; state?: Record }} options + * @param {{ + * replaceState?: boolean | undefined; + * noScroll?: boolean | undefined; + * keepFocus?: boolean | undefined; + * invalidateAll?: boolean | undefined; + * invalidate?: Array boolean)> | undefined; + * state?: Record | undefined; + * }} options * @param {number} redirect_count * @param {{}} [nav_token] */ @@ -697,7 +704,7 @@ async function initialize(result, target, hydrate) { page.status = rendering_error.status; return error; } - : undefined + : /** @type {never} */ (undefined) }); // Wait for a microtask in case svelte experimental async is enabled, @@ -731,11 +738,11 @@ async function initialize(result, target, hydrate) { * url: URL; * params: Record; * branch: Array; - * errors?: Array; + * errors?: Array | undefined; * status: number; * error: App.Error | null; * route: import('types').CSRRoute | null; - * form?: Record | null; + * form?: Record | null | undefined; * }} opts */ async function get_navigation_result_from_branch({ @@ -962,7 +969,7 @@ async function load_node({ loader, parent, url, params, route, server_data_node // implement streaming request bodies and/or the body getter body: resource.method === 'GET' || resource.method === 'HEAD' - ? undefined + ? /** @type {never} */ (undefined) : await resource.blob(), cache: resource.cache, credentials: resource.credentials, @@ -971,7 +978,7 @@ async function load_node({ loader, parent, url, params, route, server_data_node // To keep the two values in sync, we explicitly set the headers to `undefined`. // Also, not sure why, but sometimes 0 is evaluated as truthy so we need to // explicitly compare the headers length to a number here - headers: [...resource.headers].length > 0 ? resource?.headers : undefined, + headers: [...resource.headers].length > 0 ? resource?.headers : /** @type {never} */ (undefined), integrity: resource.integrity, keepalive: resource.keepalive, method: resource.method, @@ -1650,16 +1657,16 @@ function _before_navigate({ url, type, intent, delta, event, scroll }) { * state: Record; * scroll: { x: number, y: number }; * delta: number; - * }; - * keepfocus?: boolean; - * noscroll?: boolean; - * replace_state?: boolean; - * state?: Record; - * redirect_count?: number; - * nav_token?: {}; - * accept?: () => void; - * block?: () => void; - * event?: Event + * } | undefined; + * keepfocus?: boolean | undefined; + * noscroll?: boolean | undefined; + * replace_state?: boolean | undefined; + * state?: Record | undefined; + * redirect_count?: number | undefined; + * nav_token?: {} | undefined; + * accept?: (() => void) | undefined; + * block?: (() => void) | undefined; + * event?: Event | undefined; * }} opts */ async function navigate({ diff --git a/packages/kit/src/runtime/client/types.d.ts b/packages/kit/src/runtime/client/types.d.ts index f91124f962c7..768258dd441a 100644 --- a/packages/kit/src/runtime/client/types.d.ts +++ b/packages/kit/src/runtime/client/types.d.ts @@ -100,14 +100,14 @@ export type BranchNode = { server: DataNode | null; universal: DataNode | null; data: Record | null; - slash?: TrailingSlash; + slash?: TrailingSlash | undefined; }; export interface DataNode { type: 'data'; data: Record | null; uses: Uses; - slash?: TrailingSlash; + slash?: TrailingSlash | undefined; } export interface NavigationState { diff --git a/packages/kit/src/types/internal.d.ts b/packages/kit/src/types/internal.d.ts index c64d0e46fde6..73962192c7bb 100644 --- a/packages/kit/src/types/internal.d.ts +++ b/packages/kit/src/types/internal.d.ts @@ -673,8 +673,8 @@ export interface RemoteFormInternals extends BaseRemoteInternals { export interface RemotePrerenderInternals extends BaseRemoteInternals { type: 'prerender'; has_arg: boolean; - dynamic?: boolean; - inputs?: RemotePrerenderInputsGenerator; + dynamic?: boolean | undefined; + inputs?: RemotePrerenderInputsGenerator | undefined; } export type RemoteAnyQueryInternals = From 7a2d1b3b1e75556eb1da04d1507dd86526c8ba26 Mon Sep 17 00:00:00 2001 From: EchoCrow <5488190+echocrow@users.noreply.github.com> Date: Sat, 23 May 2026 01:19:40 +0200 Subject: [PATCH 5/6] fix new type error --- packages/kit/src/exports/public.d.ts | 2 +- packages/kit/src/runtime/client/client.js | 5 ++++- packages/kit/types/index.d.ts | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/kit/src/exports/public.d.ts b/packages/kit/src/exports/public.d.ts index 31b39c82a722..7fb12f663edf 100644 --- a/packages/kit/src/exports/public.d.ts +++ b/packages/kit/src/exports/public.d.ts @@ -2090,7 +2090,7 @@ type RecursiveFormFields = RemoteFormFieldContainer & { type MaybeArray = T | T[]; export interface RemoteFormInput { - [key: string]: MaybeArray; + [key: string]: MaybeArray | undefined; } export interface RemoteFormIssue { diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js index 1c52f20aa2b4..525e096f2259 100644 --- a/packages/kit/src/runtime/client/client.js +++ b/packages/kit/src/runtime/client/client.js @@ -978,7 +978,10 @@ async function load_node({ loader, parent, url, params, route, server_data_node // To keep the two values in sync, we explicitly set the headers to `undefined`. // Also, not sure why, but sometimes 0 is evaluated as truthy so we need to // explicitly compare the headers length to a number here - headers: [...resource.headers].length > 0 ? resource?.headers : /** @type {never} */ (undefined), + headers: + [...resource.headers].length > 0 + ? resource?.headers + : /** @type {never} */ (undefined), integrity: resource.integrity, keepalive: resource.keepalive, method: resource.method, diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts index 1948fe5f4615..8443b401cb69 100644 --- a/packages/kit/types/index.d.ts +++ b/packages/kit/types/index.d.ts @@ -2064,7 +2064,7 @@ declare module '@sveltejs/kit' { type MaybeArray = T | T[]; export interface RemoteFormInput { - [key: string]: MaybeArray; + [key: string]: MaybeArray | undefined; } export interface RemoteFormIssue { From 6d6a58eb899bb59c85f32f47ab9e44605b491244 Mon Sep 17 00:00:00 2001 From: EchoCrow <5488190+echocrow@users.noreply.github.com> Date: Sat, 23 May 2026 01:20:26 +0200 Subject: [PATCH 6/6] add changeset --- .changeset/nice-mugs-mate.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/nice-mugs-mate.md diff --git a/.changeset/nice-mugs-mate.md b/.changeset/nice-mugs-mate.md new file mode 100644 index 000000000000..b6c43b0a4abc --- /dev/null +++ b/.changeset/nice-mugs-mate.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: support `exactOptionalPropertyTypes` for optional form schema fields