Skip to content
Open
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
5 changes: 5 additions & 0 deletions .changeset/nice-mugs-mate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': patch
---

fix: support `exactOptionalPropertyTypes` for optional form schema fields
2 changes: 1 addition & 1 deletion packages/kit/src/exports/public.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2090,7 +2090,7 @@ type RecursiveFormFields = RemoteFormFieldContainer<any> & {
type MaybeArray<T> = T | T[];

export interface RemoteFormInput {
[key: string]: MaybeArray<string | number | boolean | File | RemoteFormInput>;
[key: string]: MaybeArray<string | number | boolean | File | RemoteFormInput> | undefined;
}

export interface RemoteFormIssue {
Expand Down
42 changes: 26 additions & 16 deletions packages/kit/src/runtime/client/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -514,7 +514,14 @@ function persist_state() {

/**
* @param {string | URL} url
* @param {{ replaceState?: boolean; noScroll?: boolean; keepFocus?: boolean; invalidateAll?: boolean; invalidate?: Array<string | URL | ((url: URL) => boolean)>; state?: Record<string, any> }} options
* @param {{
* replaceState?: boolean | undefined;
* noScroll?: boolean | undefined;
* keepFocus?: boolean | undefined;
* invalidateAll?: boolean | undefined;
* invalidate?: Array<string | URL | ((url: URL) => boolean)> | undefined;
* state?: Record<string, any> | undefined;
* }} options
* @param {number} redirect_count
* @param {{}} [nav_token]
*/
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -731,11 +738,11 @@ async function initialize(result, target, hydrate) {
* url: URL;
* params: Record<string, string>;
* branch: Array<import('./types.js').BranchNode | undefined>;
* errors?: Array<import('types').CSRPageNodeLoader | undefined>;
* errors?: Array<import('types').CSRPageNodeLoader | undefined> | undefined;
* status: number;
* error: App.Error | null;
* route: import('types').CSRRoute | null;
* form?: Record<string, any> | null;
* form?: Record<string, any> | null | undefined;
* }} opts
*/
async function get_navigation_result_from_branch({
Expand Down Expand Up @@ -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,
Expand All @@ -971,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 : undefined,
headers:
[...resource.headers].length > 0
? resource?.headers
: /** @type {never} */ (undefined),
integrity: resource.integrity,
keepalive: resource.keepalive,
method: resource.method,
Expand Down Expand Up @@ -1650,16 +1660,16 @@ function _before_navigate({ url, type, intent, delta, event, scroll }) {
* state: Record<string, any>;
* scroll: { x: number, y: number };
* delta: number;
* };
* keepfocus?: boolean;
* noscroll?: boolean;
* replace_state?: boolean;
* state?: Record<string, any>;
* redirect_count?: number;
* nav_token?: {};
* accept?: () => void;
* block?: () => void;
* event?: Event
* } | undefined;
* keepfocus?: boolean | undefined;
* noscroll?: boolean | undefined;
* replace_state?: boolean | undefined;
* state?: Record<string, any> | undefined;
* redirect_count?: number | undefined;
* nav_token?: {} | undefined;
* accept?: (() => void) | undefined;
* block?: (() => void) | undefined;
* event?: Event | undefined;
* }} opts
*/
async function navigate({
Expand Down
4 changes: 2 additions & 2 deletions packages/kit/src/runtime/client/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,14 +100,14 @@ export type BranchNode = {
server: DataNode | null;
universal: DataNode | null;
data: Record<string, any> | null;
slash?: TrailingSlash;
slash?: TrailingSlash | undefined;
};

export interface DataNode {
type: 'data';
data: Record<string, any> | null;
uses: Uses;
slash?: TrailingSlash;
slash?: TrailingSlash | undefined;
}

export interface NavigationState {
Expand Down
4 changes: 2 additions & 2 deletions packages/kit/src/types/internal.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down
44 changes: 42 additions & 2 deletions packages/kit/test/types/remote.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> = null as any;
const schema2: StandardSchemaV1<string, number> = null as any;
const schema3: StandardSchemaV1<string | undefined, number> = null as any;
Expand Down Expand Up @@ -58,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);
Expand Down Expand Up @@ -515,6 +535,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) => {
Expand All @@ -540,8 +562,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`).
Expand Down Expand Up @@ -578,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;
Expand Down
4 changes: 3 additions & 1 deletion packages/kit/test/types/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"noEmit": true
"exactOptionalPropertyTypes": true,
"noEmit": true,
"skipLibCheck": true
},
"include": ["**/*.test.ts", "../../src/types/*.d.ts"]
}
2 changes: 1 addition & 1 deletion packages/kit/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2064,7 +2064,7 @@ declare module '@sveltejs/kit' {
type MaybeArray<T> = T | T[];

export interface RemoteFormInput {
[key: string]: MaybeArray<string | number | boolean | File | RemoteFormInput>;
[key: string]: MaybeArray<string | number | boolean | File | RemoteFormInput> | undefined;
}

export interface RemoteFormIssue {
Expand Down
Loading