From 2497cdebfaa9af9f818c1361ca55f41b76140feb Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Mon, 20 Oct 2025 09:48:28 +0200 Subject: [PATCH 1/3] 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 --- .changeset/icy-glasses-agree.md | 5 + packages/kit/src/exports/index.js | 50 ++++++- packages/kit/src/exports/internal/index.js | 16 +++ packages/kit/src/exports/public.d.ts | 22 +-- .../kit/src/runtime/app/server/remote/form.js | 136 ++++++++---------- .../remote/form/validate/form.remote.ts | 5 +- packages/kit/types/index.d.ts | 65 +++++++-- 7 files changed, 194 insertions(+), 105 deletions(-) create mode 100644 .changeset/icy-glasses-agree.md diff --git a/.changeset/icy-glasses-agree.md b/.changeset/icy-glasses-agree.md new file mode 100644 index 000000000000..2f103d4a8d9d --- /dev/null +++ b/.changeset/icy-glasses-agree.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +breaking: `invalid` now must be imported from `@sveltejs/kit` diff --git a/packages/kit/src/exports/index.js b/packages/kit/src/exports/index.js index 8663772f0c0c..0d78e017cebd 100644 --- a/packages/kit/src/exports/index.js +++ b/packages/kit/src/exports/index.js @@ -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,52 @@ 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 * as v from 'valibot'; + * + * function tryRegisterUser(name: string, password: string) { + * // ... + * } + * + * export const register = form( + * v.object({ name: v.string(), _password: v.string() }), + * async ({ name, _password }, issue) => { + * const success = tryRegisterUser(name, _password); + * if (!success) { + * invalid('Registration failed', issue.name('This username is already taken')); + * } + * + * // ... + * } + * ); + * ``` + * @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 diff --git a/packages/kit/src/exports/internal/index.js b/packages/kit/src/exports/internal/index.js index b87448b30914..8ef0a32a32c8 100644 --- a/packages/kit/src/exports/internal/index.js +++ b/packages/kit/src/exports/internal/index.js @@ -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'; diff --git a/packages/kit/src/exports/public.d.ts b/packages/kit/src/exports/public.d.ts index cb42dc6cc1c0..ef6ccef0c9d1 100644 --- a/packages/kit/src/exports/public.d.ts +++ b/packages/kit/src/exports/public.d.ts @@ -1974,10 +1974,13 @@ type ExtractId = 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 = +export type InvalidField = WillRecurseIndefinitely extends true ? Record : NonNullable extends string | number | boolean | File @@ -1993,15 +1996,12 @@ type InvalidField = : Record; /** - * 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 = ((...issues: Array) => never) & - InvalidField; +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. diff --git a/packages/kit/src/runtime/app/server/remote/form.js b/packages/kit/src/runtime/app/server/remote/form.js index 9d5b5b551a4d..e90a7ce7668b 100644 --- a/packages/kit/src/runtime/app/server/remote/form.js +++ b/packages/kit/src/runtime/app/server/remote/form.js @@ -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.svelte.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 `
` element. @@ -21,7 +22,7 @@ import { get_cache, run_remote_function } from './shared.js'; * * @template Output * @overload - * @param {(invalid: import('@sveltejs/kit').Invalid) => MaybePromise} fn + * @param {() => MaybePromise} fn * @returns {RemoteForm} * @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) => MaybePromise} fn + * @param {(data: Input, issue: InvalidField) => MaybePromise} fn * @returns {RemoteForm} * @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, invalid: import('@sveltejs/kit').Invalid>) => MaybePromise} fn + * @param {(data: StandardSchemaV1.InferOutput, issue: InvalidField>) => MaybePromise} fn * @returns {RemoteForm, 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} [maybe_fn] + * @param {(data_or_issue: any, issue?: any) => MaybePromise} [maybe_fn] * @returns {RemoteForm} * @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) { @@ -329,89 +330,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} */ -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} */ ( + 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 validaition issues. ' + + 'Keep using the parameter (now named issue) provided to the form function only to construct the 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); + } + }); + } } diff --git a/packages/kit/test/apps/basics/src/routes/remote/form/validate/form.remote.ts b/packages/kit/test/apps/basics/src/routes/remote/form/validate/form.remote.ts index 10df815353a7..a823759c3dff 100644 --- a/packages/kit/test/apps/basics/src/routes/remote/form/validate/form.remote.ts +++ b/packages/kit/test/apps/basics/src/routes/remote/form/validate/form.remote.ts @@ -1,4 +1,5 @@ import { form } from '$app/server'; +import { invalid } from '@sveltejs/kit'; import * as v from 'valibot'; export const my_form = form( @@ -7,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); diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts index 52fcb547490d..6c7a01caf742 100644 --- a/packages/kit/types/index.d.ts +++ b/packages/kit/types/index.d.ts @@ -1950,10 +1950,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 = + export type InvalidField = WillRecurseIndefinitely extends true ? Record : NonNullable extends string | number | boolean | File @@ -1969,15 +1972,12 @@ declare module '@sveltejs/kit' { : Record; /** - * 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 = ((...issues: Array) => never) & - InvalidField; + 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. @@ -2683,6 +2683,41 @@ 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 * as v from 'valibot'; + * + * function tryRegisterUser(name: string, password: string) { + * // ... + * } + * + * export const register = form( + * v.object({ name: v.string(), _password: v.string() }), + * async ({ name, _password }, issue) => { + * const success = tryRegisterUser(name, _password); + * if (!success) { + * invalid('Registration failed', issue.name('This username is already taken')); + * } + * + * // ... + * } + * ); + * ``` + * @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 @@ -3115,7 +3150,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 @@ -3169,7 +3204,7 @@ declare module '$app/server' { * * @since 2.27 */ - export function form(fn: (invalid: import("@sveltejs/kit").Invalid) => MaybePromise): RemoteForm; + export function form(fn: () => MaybePromise): RemoteForm; /** * Creates a form object that can be spread onto a `` element. * @@ -3177,7 +3212,7 @@ declare module '$app/server' { * * @since 2.27 */ - export function form(validate: "unchecked", fn: (data: Input, invalid: import("@sveltejs/kit").Invalid) => MaybePromise): RemoteForm; + export function form(validate: "unchecked", fn: (data: Input, issue: InvalidField) => MaybePromise): RemoteForm; /** * Creates a form object that can be spread onto a `` element. * @@ -3185,7 +3220,7 @@ declare module '$app/server' { * * @since 2.27 */ - export function form>, Output>(validate: Schema, fn: (data: StandardSchemaV1.InferOutput, invalid: import("@sveltejs/kit").Invalid>) => MaybePromise): RemoteForm, Output>; + export function form>, Output>(validate: Schema, fn: (data: StandardSchemaV1.InferOutput, issue: InvalidField>) => MaybePromise): RemoteForm, Output>; /** * Creates a remote prerender function. When called from the browser, the function will be invoked on the server via a `fetch` call. * From 48045120b861f19e6bb26b2636c48ebab95d1393 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Mon, 20 Oct 2025 09:57:09 +0200 Subject: [PATCH 2/3] docs --- .../docs/20-core-concepts/60-remote-functions.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/documentation/docs/20-core-concepts/60-remote-functions.md b/documentation/docs/20-core-concepts/60-remote-functions.md index 969b90d17121..bf1bbcf69aab 100644 --- a/documentation/docs/20-core-concepts/60-remote-functions.md +++ b/documentation/docs/20-core-concepts/60-remote-functions.md @@ -452,11 +452,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 standard-schema-compliant issues. 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'; @@ -467,13 +468,17 @@ 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`) + // This will show up on the root issue list + 'Purchase failed', + // Creates a `{ message: ..., path: ['qty'] }` object, + // will show up on the issue list for the `qty` field + issue.qty(`we don't have enough hotcakes`) ); } } From 0cb5edc0fe2e08d9114c2b591deb361ad1fc0238 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Mon, 20 Oct 2025 10:12:50 +0200 Subject: [PATCH 3/3] fix test --- packages/kit/test/types/remote.test.ts | 116 +++++++++++++------------ 1 file changed, 62 insertions(+), 54 deletions(-) diff --git a/packages/kit/test/types/remote.test.ts b/packages/kit/test/types/remote.test.ts index 760663fa9291..605645254919 100644 --- a/packages/kit/test/types/remote.test.ts +++ b/packages/kit/test/types/remote.test.ts @@ -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 = null as any; const schema2: StandardSchemaV1 = 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>, (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'); }); // @ts-expect-error f8.fields.x;