From aa6e5e0abf09ce991c95ef504f11f0de077d33a0 Mon Sep 17 00:00:00 2001 From: Christopher Pruijsen Date: Fri, 11 Sep 2026 16:26:42 +0100 Subject: [PATCH] fix(form-core): infer form error types from onServerValidate in formOptions Specify onServerValidate on FormOptions so shared formOptions flow the server validator return type into form-level errors. Fixes #1325 --- .changeset/form-errors-on-server.md | 5 ++++ docs/framework/react/guides/ssr.md | 12 ++++---- .../next-server-actions/src/app/action.ts | 5 ---- .../src/app/client-component.tsx | 2 +- .../src/app/shared-code.ts | 5 ++++ packages/form-core/src/FormApi.ts | 6 ++++ .../form-core/tests/formOptions.test-d.ts | 29 +++++++++++++++++++ packages/react-form/tests/useForm.test-d.tsx | 22 ++++++++++++++ 8 files changed, 74 insertions(+), 12 deletions(-) create mode 100644 .changeset/form-errors-on-server.md diff --git a/.changeset/form-errors-on-server.md b/.changeset/form-errors-on-server.md new file mode 100644 index 0000000000..ac4dbf73c3 --- /dev/null +++ b/.changeset/form-errors-on-server.md @@ -0,0 +1,5 @@ +--- +'@tanstack/form-core': patch +--- + +Infer form-level `errors` from `onServerValidate` specified in `formOptions`. diff --git a/docs/framework/react/guides/ssr.md b/docs/framework/react/guides/ssr.md index 9013cb873d..bd711e5215 100644 --- a/docs/framework/react/guides/ssr.md +++ b/docs/framework/react/guides/ssr.md @@ -200,6 +200,11 @@ export const formOpts = formOptions({ firstName: '', age: 0, }, + onServerValidate: ({ value }) => { + if (value.age < 12) { + return 'Server validation: You must be at least 12 to sign up' + } + }, }) ``` @@ -219,11 +224,6 @@ import { formOpts } from './shared-code' // Create the server action that will infer the types of the form from `formOpts` const serverValidate = createServerValidate({ ...formOpts, - onServerValidate: ({ value }) => { - if (value.age < 12) { - return 'Server validation: You must be at least 12 to sign up' - } - }, }) export default async function someAction(prev: unknown, formData: FormData) { @@ -278,7 +278,7 @@ export const ClientComp = () => { return (
form.handleSubmit()}> {formErrors.map((error) => ( -

{error}

+

{error}

))} { - if (value.age < 12) { - return 'Server validation: You must be at least 12 to sign up' - } - }, }) export default async function someAction(prev: unknown, formData: FormData) { diff --git a/examples/react/next-server-actions/src/app/client-component.tsx b/examples/react/next-server-actions/src/app/client-component.tsx index 2b071ef532..3f9b33c0ff 100644 --- a/examples/react/next-server-actions/src/app/client-component.tsx +++ b/examples/react/next-server-actions/src/app/client-component.tsx @@ -27,7 +27,7 @@ export const ClientComp = () => { return ( form.handleSubmit()}> {formErrors.map((error) => ( -

{error}

+

{error}

))} { + if (value.age < 12) { + return 'Server validation: You must be at least 12 to sign up' + } + }, }) diff --git a/packages/form-core/src/FormApi.ts b/packages/form-core/src/FormApi.ts index 4400017d30..302c0d11b7 100644 --- a/packages/form-core/src/FormApi.ts +++ b/packages/form-core/src/FormApi.ts @@ -510,6 +510,12 @@ export interface FormOptions< TOnDynamic, TOnDynamicAsync > + /** + * Server-only validator used by `createServerValidate`. Specify it in shared + * `formOptions` so the client form infers `state.errors` / `errorMap.onServer` + * from the same function. This is not executed in the browser. + */ + onServerValidate?: TOnServer validationLogic?: ValidationLogicFn diff --git a/packages/form-core/tests/formOptions.test-d.ts b/packages/form-core/tests/formOptions.test-d.ts index fd8089549f..46d96348de 100644 --- a/packages/form-core/tests/formOptions.test-d.ts +++ b/packages/form-core/tests/formOptions.test-d.ts @@ -319,4 +319,33 @@ describe('formOptions', () => { ('Too short!' | 'I just need an error')[] >() }) + + it('should infer form error types from onServerValidate in formOptions', () => { + const formOpts = formOptions({ + defaultValues: { + firstName: '', + age: 0, + }, + onServerValidate: ({ value }) => { + expectTypeOf(value.age).toEqualTypeOf() + if (value.age < 12) { + return 'Server validation: You must be at least 12 to sign up' as const + } + return undefined + }, + }) + + const form = new FormApi(formOpts) + expectTypeOf(form.state.errors).toEqualTypeOf< + 'Server validation: You must be at least 12 to sign up'[] + >() + expectTypeOf(form.state.errorMap.onServer).toEqualTypeOf< + 'Server validation: You must be at least 12 to sign up' | undefined + >() + + const form2 = new FormApi({ ...formOpts }) + expectTypeOf(form2.state.errors).toEqualTypeOf< + 'Server validation: You must be at least 12 to sign up'[] + >() + }) }) diff --git a/packages/react-form/tests/useForm.test-d.tsx b/packages/react-form/tests/useForm.test-d.tsx index 2e92cf29ce..4e86c0df76 100644 --- a/packages/react-form/tests/useForm.test-d.tsx +++ b/packages/react-form/tests/useForm.test-d.tsx @@ -154,4 +154,26 @@ describe('useForm', () => { expectTypeOf(form.state.values).toExtend() }) + + it('should infer form error types from onServerValidate in formOptions', () => { + const formOpts = formOptions({ + defaultValues: { + firstName: '', + age: 0, + }, + onServerValidate: ({ value }) => { + expectTypeOf(value.age).toEqualTypeOf() + if (value.age < 12) { + return 'Server validation: You must be at least 12 to sign up' as const + } + return undefined + }, + }) + + const form = useForm({ ...formOpts }) + + expectTypeOf(form.state.errors).toEqualTypeOf< + 'Server validation: You must be at least 12 to sign up'[] + >() + }) })