From 276deadbc7489f1a51f088a4e4fc447a700c113e Mon Sep 17 00:00:00 2001 From: Dharya-dev Date: Sat, 30 May 2026 00:43:54 +0530 Subject: [PATCH] fix(form-core): fix validator type inference in formOptions When using formOptions() with validators, the callback parameter types were inferred as 'any' instead of the expected typed shape (e.g. data.value was any instead of TFormData). Root cause: the return type 'TOptions' captured the raw object literal type before TypeScript could resolve the validator generics through FormOptions. Fix: use Omit+Pick to selectively re-type the validator-dependent properties (validators, onSubmit, onSubmitInvalid, listeners) with properly resolved generic types wrapped in NoInfer, while keeping TOptions for all other properties to preserve spread/override compatibility. Added type-level tests that verify: - validators.onSubmit data.value is typed correctly (not any) - onSubmit handler data.value is typed correctly (not any) - spreading formOpts into FormApi still works - overriding validators when spreading still works Closes #1613 --- .../fix-form-options-validator-inference.md | 15 +++++ packages/form-core/src/formOptions.ts | 34 +++++++++- .../form-core/tests/formOptions.test-d.ts | 67 +++++++++++++++++++ 3 files changed, 114 insertions(+), 2 deletions(-) create mode 100644 .changeset/fix-form-options-validator-inference.md diff --git a/.changeset/fix-form-options-validator-inference.md b/.changeset/fix-form-options-validator-inference.md new file mode 100644 index 000000000..0cf6dac9c --- /dev/null +++ b/.changeset/fix-form-options-validator-inference.md @@ -0,0 +1,15 @@ +--- +'@tanstack/form-core': patch +--- + +fix(form-core): fix validator type inference in formOptions + +When using `formOptions()` with `validators`, the callback parameter types were +inferred as `any` instead of the expected typed shape. This happened because the +return type `TOptions` captured the raw object literal type before TypeScript could +resolve the validator generics. + +The fix uses `Omit` + `Pick` to selectively re-type the validator-dependent properties +(`validators`, `onSubmit`, `onSubmitInvalid`, `listeners`) with the properly resolved +generic types, while preserving spread/override compatibility through `TOptions` for +all other properties. diff --git a/packages/form-core/src/formOptions.ts b/packages/form-core/src/formOptions.ts index e77460e6c..acd88f6bb 100644 --- a/packages/form-core/src/formOptions.ts +++ b/packages/form-core/src/formOptions.ts @@ -18,8 +18,19 @@ inside of it, meaning that TFormData changes to `unknown`. To bypass this, the intersection for defaultOpts gives TypeScript that information again, without losing the benefits from the TOptions generic. + +The return type uses Pick to selectively overlay the resolved validator/listener/onSubmit +types onto TOptions. This preserves spread compatibility while ensuring callbacks +have correctly typed parameters. We avoid intersecting the full FormOptions to prevent +"excessively deep" errors from `in out` variance annotations. */ +/** + * Keys on FormOptions whose types depend on the validator generics + * and need to be resolved for proper callback parameter inference. + */ +type FormOptionsDependentKeys = 'validators' | 'onSubmit' | 'onSubmitInvalid' | 'listeners' + export function formOptions< TOptions extends Partial< FormOptions< @@ -67,6 +78,25 @@ export function formOptions< > > & TOptions, -): TOptions { - return defaultOpts +): Omit & + Pick< + Partial< + FormOptions< + NoInfer, + NoInfer, + NoInfer, + NoInfer, + NoInfer, + NoInfer, + NoInfer, + NoInfer, + NoInfer, + NoInfer, + NoInfer, + NoInfer + > + >, + FormOptionsDependentKeys & keyof TOptions + > { + return defaultOpts as any } diff --git a/packages/form-core/tests/formOptions.test-d.ts b/packages/form-core/tests/formOptions.test-d.ts index fd8089549..79b2a1cc7 100644 --- a/packages/form-core/tests/formOptions.test-d.ts +++ b/packages/form-core/tests/formOptions.test-d.ts @@ -319,4 +319,71 @@ describe('formOptions', () => { ('Too short!' | 'I just need an error')[] >() }) + + it('validators.onSubmit data param should NOT be any when defined in formOptions (#1613)', () => { + type FormData = { + description: string + } + + const formOpts = formOptions({ + defaultValues: { + description: '', + } as FormData, + validators: { + onSubmit: (data) => { + // The core of #1613: data.value should be FormData, NOT any + expectTypeOf(data.value).toEqualTypeOf() + expectTypeOf(data.value).not.toBeAny() + + if (data.value.description.length === 0) { + return 'Description is required' + } + return undefined + }, + }, + }) + + const form = new FormApi(formOpts) + expectTypeOf(form.state.values).toEqualTypeOf() + + // Also verify spreading still works + const form2 = new FormApi({ ...formOpts }) + expectTypeOf(form2.state.values).toEqualTypeOf() + }) + + it('onSubmit handler data param should NOT be any when defined in formOptions (#1613)', () => { + type FormData = { + rows: { id: string }[] + } + + const formOpts = formOptions({ + defaultValues: { + rows: [] as { id: string }[], + } as FormData, + onSubmit: (data) => { + // Verify the onSubmit handler parameter is typed + expectTypeOf(data.value).toEqualTypeOf() + expectTypeOf(data.value).not.toBeAny() + }, + validators: { + onSubmit: (data) => { + expectTypeOf(data.value).toEqualTypeOf() + expectTypeOf(data.value).not.toBeAny() + if (data.value.rows.length < 2) { + return 'Need at least 2 rows' + } + return undefined + }, + }, + }) + + // Should work without type errors when passed to FormApi + const form = new FormApi(formOpts) + expectTypeOf(form.state.values).toEqualTypeOf() + + // Spreading should also work + const form2 = new FormApi({ ...formOpts }) + expectTypeOf(form2.state.values).toEqualTypeOf() + }) }) +