breaking: `invalid` now must be imported from `@sveltejs/kit` (#14768)
authorSimon H <5968653+dummdidumm@users.noreply.github.com>
Thu, 20 Nov 2025 21:31:29 +0000 (22:31 +0100)
committerGitHub <noreply@github.com>
Thu, 20 Nov 2025 21:31:29 +0000 (16:31 -0500)
* breaking: `invalid` now must be imported from `@sveltejs/kit`

TypeScript kinda forced our hand here - due to limitations of control flow analysis it does not detect the `never` return type for anything else than functions that are used directly (i.e. passing a function as a parameter doesn't work unless you explicitly type it); see https://github.com/microsoft/TypeScript/issues/36753 for more info.

This therefore changes `invalid` to be a function that you import just like `redirect` or `error`. A nice benefit of this is that you'll no longer have to use the second parameter passed to remote form functions to construct the list of issues in case you want to create an issue for the whole form and not just a specific field.

Closes #14745

* docs

* fix test

* Apply suggestions from code review

* Update packages/kit/src/runtime/app/server/remote/form.js

* Update documentation/docs/20-core-concepts/60-remote-functions.md

* prettier

* regenerate

* fix

---------

Co-authored-by: Rich Harris <richard.a.harris@gmail.com>
Co-authored-by: Rich Harris <rich.harris@vercel.com>
.changeset/icy-glasses-agree.md [new file with mode: 0644]
documentation/docs/20-core-concepts/60-remote-functions.md
packages/kit/src/exports/index.js
packages/kit/src/exports/internal/index.js
packages/kit/src/exports/public.d.ts
packages/kit/src/runtime/app/server/remote/form.js
packages/kit/test/apps/basics/src/routes/remote/form/validate/form.remote.ts
packages/kit/test/types/remote.test.ts
packages/kit/types/index.d.ts

diff --git a/.changeset/icy-glasses-agree.md b/.changeset/icy-glasses-agree.md
new file mode 100644 (file)
index 0000000..2f103d4
--- /dev/null
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+breaking: `invalid` now must be imported from `@sveltejs/kit`
index bcbb3e9c157036b9987b9d9ad0b7d04da6b8c262..e70e4f33cc134742a18e2d876c5bf90a8ce4fb72 100644 (file)
@@ -454,11 +454,12 @@ Alternatively, you could use `select` and `select multiple`:
 
 ### Programmatic validation
 
-In addition to declarative schema validation, you can programmatically mark fields as invalid inside the form handler using the `invalid` function. This is useful for cases where you can't know if something is valid until you try to perform some action:
+In addition to declarative schema validation, you can programmatically mark fields as invalid inside the form handler using the `invalid` function. This is useful for cases where you can't know if something is valid until you try to perform some action. Just like `redirect` or `error`, `invalid` throws. It expects a list of strings (for issues relating to the form as a whole) or standard-schema-compliant issues (for those relating to a specific field). Use the `issue` parameter for type-safe creation of such issues:
 
 ```js
 /// file: src/routes/shop/data.remote.js
 import * as v from 'valibot';
+import { invalid } from '@sveltejs/kit';
 import { form } from '$app/server';
 import * as db from '$lib/server/database';
 
@@ -469,13 +470,13 @@ export const buyHotcakes = form(
                        v.minValue(1, 'you must buy at least one hotcake')
                )
        }),
-       async (data, invalid) => {
+       async (data, issue) => {
                try {
                        await db.buy(data.qty);
                } catch (e) {
                        if (e.code === 'OUT_OF_STOCK') {
                                invalid(
-                                       invalid.qty(`we don't have enough hotcakes`)
+                                       issue.qty(`we don't have enough hotcakes`)
                                );
                        }
                }
index 8663772f0c0c0ec2521562b990357cac3239fbed..8923ea96a3a6c27b93671af6f566f6186d419de3 100644 (file)
@@ -1,4 +1,6 @@
-import { HttpError, Redirect, ActionFailure } from './internal/index.js';
+/** @import { StandardSchemaV1 } from '@standard-schema/spec' */
+
+import { HttpError, Redirect, ActionFailure, ValidationError } from './internal/index.js';
 import { BROWSER, DEV } from 'esm-env';
 import {
        add_data_suffix,
@@ -215,6 +217,49 @@ export function isActionFailure(e) {
        return e instanceof ActionFailure;
 }
 
+/**
+ * Use this to throw a validation error to imperatively fail form validation.
+ * Can be used in combination with `issue` passed to form actions to create field-specific issues.
+ *
+ * @example
+ * ```ts
+ * import { invalid } from '@sveltejs/kit';
+ * import { form } from '$app/server';
+ * import { tryLogin } from '$lib/server/auth';
+ * import * as v from 'valibot';
+ *
+ * export const login = form(
+ *   v.object({ name: v.string(), _password: v.string() }),
+ *   async ({ name, _password }) => {
+ *     const success = tryLogin(name, _password);
+ *     if (!success) {
+ *       invalid('Incorrect username or password');
+ *     }
+ *
+ *     // ...
+ *   }
+ * );
+ * ```
+ * @param  {...(StandardSchemaV1.Issue | string)} issues
+ * @returns {never}
+ * @since 2.47.3
+ */
+export function invalid(...issues) {
+       throw new ValidationError(
+               issues.map((issue) => (typeof issue === 'string' ? { message: issue } : issue))
+       );
+}
+
+/**
+ * Checks whether this is an validation error thrown by {@link invalid}.
+ * @param {unknown} e The object to check.
+ * @return {e is import('./public.js').ActionFailure}
+ * @since 2.47.3
+ */
+export function isValidationError(e) {
+       return e instanceof ValidationError;
+}
+
 /**
  * Strips possible SvelteKit-internal suffixes and trailing slashes from the URL pathname.
  * Returns the normalized URL as well as a method for adding the potential suffix back
index b87448b30914bf752b52260b36cd0787abcb7e91..8ef0a32a32c8b141821588b99de06452cdb1eb03 100644 (file)
@@ -1,3 +1,5 @@
+/** @import { StandardSchemaV1 } from '@standard-schema/spec' */
+
 export class HttpError {
        /**
         * @param {number} status
@@ -62,4 +64,18 @@ export class ActionFailure {
        }
 }
 
+/**
+ * Error thrown when form validation fails imperatively
+ */
+export class ValidationError extends Error {
+       /**
+        * @param {StandardSchemaV1.Issue[]} issues
+        */
+       constructor(issues) {
+               super('Validation failed');
+               this.name = 'ValidationError';
+               this.issues = issues;
+       }
+}
+
 export { init_remote_functions } from './remote-functions.js';
index 5bc86276b4f524ff6b183eca0a52014a48fa13f5..d0f3663fe1711b0bd89ba26612f2d1f3d70c6d26 100644 (file)
@@ -1992,10 +1992,13 @@ type ExtractId<Input> = Input extends { id: infer Id }
        : string | number;
 
 /**
- * Recursively maps an input type to a structure where each field can create a validation issue.
- * This mirrors the runtime behavior of the `invalid` proxy passed to form handlers.
+ * A function and proxy object used to imperatively create validation errors in form handlers.
+ *
+ * Access properties to create field-specific issues: `issue.fieldName('message')`.
+ * The type structure mirrors the input data structure for type-safe field access.
+ * Call `invalid(issue.foo(...), issue.nested.bar(...))` to throw a validation error.
  */
-type InvalidField<T> =
+export type InvalidField<T> =
        WillRecurseIndefinitely<T> extends true
                ? Record<string | number, any>
                : NonNullable<T> extends string | number | boolean | File
@@ -2011,15 +2014,12 @@ type InvalidField<T> =
                                        : Record<string, never>;
 
 /**
- * A function and proxy object used to imperatively create validation errors in form handlers.
- *
- * Call `invalid(issue1, issue2, ...issueN)` to throw a validation error.
- * If an issue is a `string`, it applies to the form as a whole (and will show up in `fields.allIssues()`)
- * Access properties to create field-specific issues: `invalid.fieldName('message')`.
- * The type structure mirrors the input data structure for type-safe field access.
+ * A validation error thrown by `invalid`.
  */
-export type Invalid<Input = any> = ((...issues: Array<string | StandardSchemaV1.Issue>) => never) &
-       InvalidField<Input>;
+export interface ValidationError {
+       /** The validation issues */
+       issues: StandardSchemaV1.Issue[];
+}
 
 /**
  * The return value of a remote `form` function. See [Remote functions](https://svelte.dev/docs/kit/remote-functions#form) for full documentation.
index c1fe5a1d2c10a5f7e6b0abb75245722fb40e434c..b23517823c153d83ad65c356f73a47d5955edcb2 100644 (file)
@@ -1,4 +1,4 @@
-/** @import { RemoteFormInput, RemoteForm } from '@sveltejs/kit' */
+/** @import { RemoteFormInput, RemoteForm, InvalidField } from '@sveltejs/kit' */
 /** @import { InternalRemoteFormIssue, MaybePromise, RemoteInfo } from 'types' */
 /** @import { StandardSchemaV1 } from '@standard-schema/spec' */
 import { get_request_store } from '@sveltejs/kit/internal/server';
@@ -13,6 +13,7 @@ import {
        flatten_issues
 } from '../../../form-utils.js';
 import { get_cache, run_remote_function } from './shared.js';
+import { ValidationError } from '@sveltejs/kit/internal';
 
 /**
  * Creates a form object that can be spread onto a `<form>` element.
@@ -21,7 +22,7 @@ import { get_cache, run_remote_function } from './shared.js';
  *
  * @template Output
  * @overload
- * @param {(invalid: import('@sveltejs/kit').Invalid<void>) => MaybePromise<Output>} fn
+ * @param {() => MaybePromise<Output>} fn
  * @returns {RemoteForm<void, Output>}
  * @since 2.27
  */
@@ -34,7 +35,7 @@ import { get_cache, run_remote_function } from './shared.js';
  * @template Output
  * @overload
  * @param {'unchecked'} validate
- * @param {(data: Input, invalid: import('@sveltejs/kit').Invalid<Input>) => MaybePromise<Output>} fn
+ * @param {(data: Input, issue: InvalidField<Input>) => MaybePromise<Output>} fn
  * @returns {RemoteForm<Input, Output>}
  * @since 2.27
  */
@@ -47,7 +48,7 @@ import { get_cache, run_remote_function } from './shared.js';
  * @template Output
  * @overload
  * @param {Schema} validate
- * @param {(data: StandardSchemaV1.InferOutput<Schema>, invalid: import('@sveltejs/kit').Invalid<StandardSchemaV1.InferInput<Schema>>) => MaybePromise<Output>} fn
+ * @param {(data: StandardSchemaV1.InferOutput<Schema>, issue: InvalidField<StandardSchemaV1.InferInput<Schema>>) => MaybePromise<Output>} fn
  * @returns {RemoteForm<StandardSchemaV1.InferInput<Schema>, Output>}
  * @since 2.27
  */
@@ -55,7 +56,7 @@ import { get_cache, run_remote_function } from './shared.js';
  * @template {RemoteFormInput} Input
  * @template Output
  * @param {any} validate_or_fn
- * @param {(data_or_invalid: any, invalid?: any) => MaybePromise<Output>} [maybe_fn]
+ * @param {(data_or_issue: any, issue?: any) => MaybePromise<Output>} [maybe_fn]
  * @returns {RemoteForm<Input, Output>}
  * @since 2.27
  */
@@ -165,7 +166,7 @@ export function form(validate_or_fn, maybe_fn) {
 
                                        state.refreshes ??= {};
 
-                                       const invalid = create_invalid();
+                                       const issue = create_issues();
 
                                        try {
                                                output.result = await run_remote_function(
@@ -174,7 +175,7 @@ export function form(validate_or_fn, maybe_fn) {
                                                        true,
                                                        data,
                                                        (d) => d,
-                                                       (data) => (!maybe_fn ? fn(invalid) : fn(data, invalid))
+                                                       (data) => (!maybe_fn ? fn() : fn(data, issue))
                                                );
                                        } catch (e) {
                                                if (e instanceof ValidationError) {
@@ -328,89 +329,72 @@ function handle_issues(output, issues, is_remote_request, form_data) {
 
 /**
  * Creates an invalid function that can be used to imperatively mark form fields as invalid
- * @returns {import('@sveltejs/kit').Invalid}
+ * @returns {InvalidField<any>}
  */
-function create_invalid() {
-       /**
-        * @param {...(string | StandardSchemaV1.Issue)} issues
-        * @returns {never}
-        */
-       function invalid(...issues) {
-               throw new ValidationError(
-                       issues.map((issue) => {
-                               if (typeof issue === 'string') {
-                                       return {
-                                               path: [],
-                                               message: issue
-                                       };
+function create_issues() {
+       return /** @type {InvalidField<any>} */ (
+               new Proxy(
+                       /** @param {string} message */
+                       (message) => {
+                               // TODO 3.0 remove
+                               if (typeof message !== 'string') {
+                                       throw new Error(
+                                               '`invalid` should now be imported from `@sveltejs/kit` to throw validation issues. ' +
+                                                       "The second parameter provided to the form function (renamed to `issue`) is still used to construct issues, e.g. `invalid(issue.field('message'))`. " +
+                                                       'For more info see https://github.com/sveltejs/kit/pulls/14768'
+                                       );
                                }
 
-                               return issue;
-                       })
-               );
-       }
-
-       return /** @type {import('@sveltejs/kit').Invalid} */ (
-               new Proxy(invalid, {
-                       get(target, prop) {
-                               if (typeof prop === 'symbol') return /** @type {any} */ (target)[prop];
+                               return create_issue(message);
+                       },
+                       {
+                               get(target, prop) {
+                                       if (typeof prop === 'symbol') return /** @type {any} */ (target)[prop];
 
-                               /**
-                                * @param {string} message
-                                * @param {(string | number)[]} path
-                                * @returns {StandardSchemaV1.Issue}
-                                */
-                               const create_issue = (message, path = []) => ({
-                                       message,
-                                       path
-                               });
-
-                               return create_issue_proxy(prop, create_issue, []);
+                                       return create_issue_proxy(prop, []);
+                               }
                        }
-               })
+               )
        );
-}
 
-/**
- * Error thrown when form validation fails imperatively
- */
-class ValidationError extends Error {
        /**
-        * @param {StandardSchemaV1.Issue[]} issues
+        * @param {string} message
+        * @param {(string | number)[]} path
+        * @returns {StandardSchemaV1.Issue}
         */
-       constructor(issues) {
-               super('Validation failed');
-               this.name = 'ValidationError';
-               this.issues = issues;
+       function create_issue(message, path = []) {
+               return {
+                       message,
+                       path
+               };
        }
-}
-
-/**
- * Creates a proxy that builds up a path and returns a function to create an issue
- * @param {string | number} key
- * @param {(message: string, path: (string | number)[]) => StandardSchemaV1.Issue} create_issue
- * @param {(string | number)[]} path
- */
-function create_issue_proxy(key, create_issue, path) {
-       const new_path = [...path, key];
 
        /**
-        * @param {string} message
-        * @returns {StandardSchemaV1.Issue}
+        * Creates a proxy that builds up a path and returns a function to create an issue
+        * @param {string | number} key
+        * @param {(string | number)[]} path
         */
-       const issue_func = (message) => create_issue(message, new_path);
+       function create_issue_proxy(key, path) {
+               const new_path = [...path, key];
 
-       return new Proxy(issue_func, {
-               get(target, prop) {
-                       if (typeof prop === 'symbol') return /** @type {any} */ (target)[prop];
+               /**
+                * @param {string} message
+                * @returns {StandardSchemaV1.Issue}
+                */
+               const issue_func = (message) => create_issue(message, new_path);
 
-                       // Handle array access like invalid.items[0]
-                       if (/^\d+$/.test(prop)) {
-                               return create_issue_proxy(parseInt(prop, 10), create_issue, new_path);
-                       }
+               return new Proxy(issue_func, {
+                       get(target, prop) {
+                               if (typeof prop === 'symbol') return /** @type {any} */ (target)[prop];
 
-                       // Handle property access like invalid.field.nested
-                       return create_issue_proxy(prop, create_issue, new_path);
-               }
-       });
+                               // Handle array access like invalid.items[0]
+                               if (/^\d+$/.test(prop)) {
+                                       return create_issue_proxy(parseInt(prop, 10), new_path);
+                               }
+
+                               // Handle property access like invalid.field.nested
+                               return create_issue_proxy(prop, new_path);
+                       }
+               });
+       }
 }
index 3e5b2826baa8272b82dd8c92843926459f5585f8..cf8a31d3aafc18ae3a157a0919673371550a4344 100644 (file)
@@ -1,5 +1,5 @@
 import { form } from '$app/server';
-import { error } from '@sveltejs/kit';
+import { error, invalid } from '@sveltejs/kit';
 import * as v from 'valibot';
 
 export const my_form = form(
@@ -8,10 +8,10 @@ export const my_form = form(
                bar: v.picklist(['d', 'e', 'f']),
                button: v.optional(v.literal('submitter'))
        }),
-       async (data, invalid) => {
+       async (data, issue) => {
                // Test imperative validation
                if (data.foo === 'c') {
-                       invalid(invalid.foo('Imperative: foo cannot be c'));
+                       invalid(issue.foo('Imperative: foo cannot be c'));
                }
 
                console.log(data);
index 99609c6d16a98258ed24793498f8021aa2f74479..e30f43fc17d834695c7e26561463734b8a6d160a 100644 (file)
@@ -1,6 +1,6 @@
 import { query, prerender, command, form } from '$app/server';
 import { StandardSchemaV1 } from '@standard-schema/spec';
-import { RemotePrerenderFunction, RemoteQueryFunction } from '@sveltejs/kit';
+import { RemotePrerenderFunction, RemoteQueryFunction, invalid } from '@sveltejs/kit';
 
 const schema: StandardSchemaV1<string> = null as any;
 const schema2: StandardSchemaV1<string, number> = null as any;
@@ -159,14 +159,16 @@ command_tests();
 
 function form_tests() {
        const q = query(() => '');
-       const f = form('unchecked', (data: { input: string }, invalid) => {
+       const f = form('unchecked', (data: { input: string }, issue) => {
                data.input;
-               invalid(
-                       'foo',
-                       invalid.input('bar'),
-                       // @ts-expect-error
-                       invalid.nonexistent.prop('baz')
-               );
+               if (Math.random() > 0.5) {
+                       invalid(
+                               'foo',
+                               issue.input('bar'),
+                               // @ts-expect-error
+                               issue.nonexistent.prop('baz')
+                       );
+               }
                return { success: true };
        });
 
@@ -184,7 +186,7 @@ function form_tests() {
 
        const f2 = form(
                null as any as StandardSchemaV1<{ a: string; nested: { prop: string } }>,
-               (data, invalid) => {
+               (data, issue) => {
                        data.a === '';
                        data.nested.prop === '';
                        // @ts-expect-error
@@ -193,17 +195,19 @@ function form_tests() {
                        data.nonexistent;
                        // @ts-expect-error
                        data.a === 123;
-                       invalid(
-                               'foo',
-                               invalid.nested.prop('bar'),
-                               // @ts-expect-error
-                               invalid.nonexistent.prop('baz')
-                       );
+                       if (Math.random() > 0.5) {
+                               invalid(
+                                       'foo',
+                                       issue.nested.prop('bar'),
+                                       // @ts-expect-error
+                                       issue.nonexistent.prop('baz')
+                               );
+                       }
                        return { success: true };
                }
        );
        // @ts-expect-error
-       f2.fields.name();
+       f2.fields.as('text');
        f2.fields.a.issues();
        f2.fields.nested.prop.issues();
        // @ts-expect-error
@@ -213,12 +217,12 @@ function form_tests() {
        // @ts-expect-error
        f2.fields.nonexistent.value();
        // @ts-expect-error
-       f2.fields.array[0].array.name();
+       f2.fields.array[0].array.as('text');
 
        // all schema properties optional
        const f3 = form(
                null as any as StandardSchemaV1<{ a?: string; nested?: { prop?: string } }>,
-               (data, invalid) => {
+               (data, issue) => {
                        data.a === '';
                        data.nested?.prop === '';
                        // @ts-expect-error
@@ -229,23 +233,25 @@ function form_tests() {
                        data.nonexistent;
                        // @ts-expect-error
                        data.a === 123;
-                       invalid(
-                               'foo',
-                               invalid.nested.prop('bar'),
-                               // @ts-expect-error
-                               invalid.nonexistent.prop('baz')
-                       );
+                       if (Math.random() > 0.5) {
+                               invalid(
+                                       'foo',
+                                       issue.nested.prop('bar'),
+                                       // @ts-expect-error
+                                       issue.nonexistent.prop('baz')
+                               );
+                       }
                        return { success: true };
                }
        );
        // @ts-expect-error
-       f3.fields.name();
+       f3.fields.as('text');
        f3.fields.a.issues();
        f3.fields.a.value();
        f3.fields.nested.prop.issues();
        f3.fields.nested.prop.value();
        // @ts-expect-error
-       f3.fields.nonexistent.name();
+       f3.fields.nonexistent.as('text');
 
        // index signature schema
        const f4 = form(null as any as StandardSchemaV1<Record<string, any>>, (data) => {
@@ -254,7 +260,7 @@ function form_tests() {
                return { success: true };
        });
        // @ts-expect-error
-       f4.fields.name();
+       f4.fields.as('text');
        f4.fields.a.issues();
        f4.fields.a.value();
        f4.fields.nested.prop.issues();
@@ -263,22 +269,24 @@ function form_tests() {
        // schema with union types
        const f5 = form(
                null as any as StandardSchemaV1<{ foo: 'a' | 'b'; bar: 'c' | 'd' }>,
-               (data, invalid) => {
+               (data, issue) => {
                        data.foo === 'a';
                        data.bar === 'c';
                        // @ts-expect-error
                        data.foo === 'e';
-                       invalid(
-                               'foo',
-                               invalid.bar('bar'),
-                               // @ts-expect-error
-                               invalid.nonexistent.prop('baz')
-                       );
+                       if (Math.random() > 0.5) {
+                               invalid(
+                                       'foo',
+                                       issue.bar('bar'),
+                                       // @ts-expect-error
+                                       issue.nonexistent.prop('baz')
+                               );
+                       }
                        return { success: true };
                }
        );
        // @ts-expect-error
-       f5.fields.name();
+       f5.fields.as('text');
        f5.fields.foo.issues();
        f5.fields.bar.issues();
        f5.fields.foo.value();
@@ -286,27 +294,29 @@ function form_tests() {
        // @ts-expect-error
        f5.fields.foo.value() === 'e';
        // @ts-expect-error
-       f5.fields.nonexistent.name();
+       f5.fields.nonexistent.as('text');
 
        // schema with arrays
        const f6 = form(
                null as any as StandardSchemaV1<{ array: Array<{ array: string[]; prop: string }> }>,
-               (data, invalid) => {
+               (data, issue) => {
                        data.array[0].prop === 'a';
                        data.array[0].array[0] === 'a';
                        // @ts-expect-error
                        data.array[0].array[0] === 1;
-                       invalid(
-                               'foo',
-                               invalid.array[0].prop('bar'),
-                               // @ts-expect-error
-                               invalid.nonexistent.prop('baz')
-                       );
+                       if (Math.random() > 0.5) {
+                               invalid(
+                                       'foo',
+                                       issue.array[0].prop('bar'),
+                                       // @ts-expect-error
+                                       issue.nonexistent.prop('baz')
+                               );
+                       }
                        return { success: true };
                }
        );
        // @ts-expect-error
-       f6.fields.name();
+       f6.fields.as('text');
        // @ts-expect-error
        f6.field('array[0].array');
        f6.fields.array.issues();
@@ -317,29 +327,27 @@ function form_tests() {
        f6.fields.array[0].prop.value();
        f6.fields.array[0].array.value();
        // @ts-expect-error
-       f6.fields.array[0].array.name();
+       f6.fields.array[0].array.as('text');
 
        // any
-       const f7 = form(null as any, (data, invalid) => {
+       const f7 = form(null as any, (data, issue) => {
                data.a === '';
                data.nested?.prop === '';
-               invalid('foo', invalid.nested.prop('bar'));
+               if (Math.random() > 0.5) {
+                       invalid('foo', issue.nested.prop('bar'));
+               }
                return { success: true };
        });
        // @ts-expect-error
-       f7.fields.name();
+       f7.fields.as('text');
        f7.fields.a.issues();
        f7.fields.a.value();
        f7.fields.nested.prop.issues();
        f7.fields.nested.prop.value();
 
        // no schema
-       const f8 = form((invalid) => {
-               invalid(
-                       'foo',
-                       // @ts-expect-error
-                       invalid.x('bar')
-               );
+       const f8 = form(() => {
+               invalid('foo');
        });
        f8.fields.issues();
        f8.fields.allIssues();
index cf87795ed24731d665dc70b5a73c2d45ccb73523..9ade41295dd5874bb3a374f3d1ace53dcc546d8d 100644 (file)
@@ -1968,10 +1968,13 @@ declare module '@sveltejs/kit' {
                : string | number;
 
        /**
-        * Recursively maps an input type to a structure where each field can create a validation issue.
-        * This mirrors the runtime behavior of the `invalid` proxy passed to form handlers.
+        * A function and proxy object used to imperatively create validation errors in form handlers.
+        *
+        * Access properties to create field-specific issues: `issue.fieldName('message')`.
+        * The type structure mirrors the input data structure for type-safe field access.
+        * Call `invalid(issue.foo(...), issue.nested.bar(...))` to throw a validation error.
         */
-       type InvalidField<T> =
+       export type InvalidField<T> =
                WillRecurseIndefinitely<T> extends true
                        ? Record<string | number, any>
                        : NonNullable<T> extends string | number | boolean | File
@@ -1987,15 +1990,12 @@ declare module '@sveltejs/kit' {
                                                : Record<string, never>;
 
        /**
-        * A function and proxy object used to imperatively create validation errors in form handlers.
-        *
-        * Call `invalid(issue1, issue2, ...issueN)` to throw a validation error.
-        * If an issue is a `string`, it applies to the form as a whole (and will show up in `fields.allIssues()`)
-        * Access properties to create field-specific issues: `invalid.fieldName('message')`.
-        * The type structure mirrors the input data structure for type-safe field access.
+        * A validation error thrown by `invalid`.
         */
-       export type Invalid<Input = any> = ((...issues: Array<string | StandardSchemaV1.Issue>) => never) &
-               InvalidField<Input>;
+       export interface ValidationError {
+               /** The validation issues */
+               issues: StandardSchemaV1.Issue[];
+       }
 
        /**
         * The return value of a remote `form` function. See [Remote functions](https://svelte.dev/docs/kit/remote-functions#form) for full documentation.
@@ -2702,6 +2702,38 @@ declare module '@sveltejs/kit' {
         * @param e The object to check.
         * */
        export function isActionFailure(e: unknown): e is ActionFailure;
+       /**
+        * Use this to throw a validation error to imperatively fail form validation.
+        * Can be used in combination with `issue` passed to form actions to create field-specific issues.
+        *
+        * @example
+        * ```ts
+        * import { invalid } from '@sveltejs/kit';
+        * import { form } from '$app/server';
+        * import { tryLogin } from '$lib/server/auth';
+        * import * as v from 'valibot';
+        *
+        * export const login = form(
+        *   v.object({ name: v.string(), _password: v.string() }),
+        *   async ({ name, _password }) => {
+        *     const success = tryLogin(name, _password);
+        *     if (!success) {
+        *       invalid('Incorrect username or password');
+        *     }
+        *
+        *     // ...
+        *   }
+        * );
+        * ```
+        * @since 2.47.3
+        */
+       export function invalid(...issues: (StandardSchemaV1.Issue | string)[]): never;
+       /**
+        * Checks whether this is an validation error thrown by {@link invalid}.
+        * @param e The object to check.
+        * @since 2.47.3
+        */
+       export function isValidationError(e: unknown): e is ActionFailure;
        /**
         * Strips possible SvelteKit-internal suffixes and trailing slashes from the URL pathname.
         * Returns the normalized URL as well as a method for adding the potential suffix back
@@ -3134,7 +3166,7 @@ declare module '$app/paths' {
 }
 
 declare module '$app/server' {
-       import type { RequestEvent, RemoteCommand, RemoteForm, RemoteFormInput, RemotePrerenderFunction, RemoteQueryFunction } from '@sveltejs/kit';
+       import type { RequestEvent, RemoteCommand, RemoteForm, RemoteFormInput, InvalidField, RemotePrerenderFunction, RemoteQueryFunction } from '@sveltejs/kit';
        import type { StandardSchemaV1 } from '@standard-schema/spec';
        /**
         * Read the contents of an imported asset from the filesystem
@@ -3188,7 +3220,7 @@ declare module '$app/server' {
         *
         * @since 2.27
         */
-       export function form<Output>(fn: (invalid: import("@sveltejs/kit").Invalid<void>) => MaybePromise<Output>): RemoteForm<void, Output>;
+       export function form<Output>(fn: () => MaybePromise<Output>): RemoteForm<void, Output>;
        /**
         * Creates a form object that can be spread onto a `<form>` element.
         *
@@ -3196,7 +3228,7 @@ declare module '$app/server' {
         *
         * @since 2.27
         */
-       export function form<Input extends RemoteFormInput, Output>(validate: "unchecked", fn: (data: Input, invalid: import("@sveltejs/kit").Invalid<Input>) => MaybePromise<Output>): RemoteForm<Input, Output>;
+       export function form<Input extends RemoteFormInput, Output>(validate: "unchecked", fn: (data: Input, issue: InvalidField<Input>) => MaybePromise<Output>): RemoteForm<Input, Output>;
        /**
         * Creates a form object that can be spread onto a `<form>` element.
         *
@@ -3204,7 +3236,7 @@ declare module '$app/server' {
         *
         * @since 2.27
         */
-       export function form<Schema extends StandardSchemaV1<RemoteFormInput, Record<string, any>>, Output>(validate: Schema, fn: (data: StandardSchemaV1.InferOutput<Schema>, invalid: import("@sveltejs/kit").Invalid<StandardSchemaV1.InferInput<Schema>>) => MaybePromise<Output>): RemoteForm<StandardSchemaV1.InferInput<Schema>, Output>;
+       export function form<Schema extends StandardSchemaV1<RemoteFormInput, Record<string, any>>, Output>(validate: Schema, fn: (data: StandardSchemaV1.InferOutput<Schema>, issue: InvalidField<StandardSchemaV1.InferInput<Schema>>) => MaybePromise<Output>): RemoteForm<StandardSchemaV1.InferInput<Schema>, Output>;
        /**
         * Creates a remote prerender function. When called from the browser, the function will be invoked on the server via a `fetch` call.
         *