Skip to content

fix(form-core): fix validator type inference in formOptions#2198

Open
Dharya-dev wants to merge 1 commit into
TanStack:mainfrom
Dharya-dev:fix/form-options-validator-type-inference
Open

fix(form-core): fix validator type inference in formOptions#2198
Dharya-dev wants to merge 1 commit into
TanStack:mainfrom
Dharya-dev:fix/form-options-validator-type-inference

Conversation

@Dharya-dev
Copy link
Copy Markdown

@Dharya-dev Dharya-dev commented May 29, 2026

🎯 Changes

Fixes formOptions() validator callback parameters being typed as any instead of the correctly inferred form data shape.

Before (broken):

const formOpts = formOptions({
  defaultValues: { description: '' },
  validators: {
    onSubmit: (data) => {
      // data.value is `any` ❌
    }
  }
})

After (fixed):

const formOpts = formOptions({
  defaultValues: { description: '' },
  validators: {
    onSubmit: (data) => {
      // data.value is `{ description: string }` ✅
    }
  }
})

Root Cause

The return type of formOptions() was TOptions, which captured the raw object literal type before TypeScript could resolve the validator generics through FormOptions. This meant callback parameter types (like data in onSubmit: (data) => ...) lost their connection to TFormData.

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.

This approach avoids the "excessively deep" type instantiation errors that occur when intersecting the full FormOptions type (due to in out variance annotations).

What's been tested

  • All existing type tests pass on TS 5.4, 5.8, and 5.9
  • All existing unit tests pass (145/145)
  • Added two new 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

✅ Checklist

  • I have followed the steps in the Contributing guide.
  • I have tested this code locally with pnpm test:pr.

🚀 Release Impact

  • This change affects published code, and I have generated a changeset.
  • This change is docs/CI/dev-only (no release).

Summary by CodeRabbit

  • Bug Fixes
    • Fixed TypeScript type inference for form validators and callbacks—callback parameters now correctly infer the expected form data types instead of defaulting to any, improving type safety and developer experience.

Review Change Stack

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 TanStack#1613
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 29, 2026

📝 Walkthrough

Walkthrough

This PR fixes a TypeScript type inference bug in formOptions() where validator callback parameters were inferred as any. The fix introduces a helper type to identify generic-dependent FormOptions keys, reworks the return type using Omit/Pick with NoInfer to preserve proper callback parameter types, adds type-level tests validating the fix, and documents the change in a changeset.

Changes

Validator callback type inference in formOptions

Layer / File(s) Summary
Type inference helper and formOptions signature
packages/form-core/src/formOptions.ts
Introduces FormOptionsDependentKeys type listing the keys (validators, onSubmit, onSubmitInvalid, listeners) that depend on validator generics, and reworks formOptions return type to omit these from TOptions then re-add them via Pick<Partial<FormOptions<NoInfer<...>>>> so callback parameters retain proper type inference.
Type-level test cases
packages/form-core/tests/formOptions.test-d.ts
Adds two test blocks verifying that validators.onSubmit and top-level onSubmit callbacks receive properly typed data.value (the concrete FormData shape, not any), and confirm the same inference holds when spreading formOpts into new FormApi(...).
Changeset documentation
.changeset/fix-form-options-validator-inference.md
Changelog entry documenting the validator type inference fix and its approach via re-typing with Omit/Pick generics.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 A type, once lost in any-land so deep,
Now wakes with proper shape its promises to keep,
With NoInfer's gentle touch and Pick's precise hand,
FormOptions validators stand proudly, firmly grand!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: fixing validator type inference in formOptions.
Description check ✅ Passed The description comprehensively covers changes with code examples, root cause analysis, the fix approach, and testing details. All template sections are completed with relevant information.
Linked Issues check ✅ Passed The PR directly addresses issue #1613 by fixing validator callback parameters to be properly typed with the form data shape instead of any, as verified by the added type-level tests.
Out of Scope Changes check ✅ Passed All changes are directly related to fixing the validator type inference issue: the changeset documents the fix, the TypeScript signature is updated to resolve the generics correctly, and new tests verify the fix works as intended.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/form-core/src/formOptions.ts`:
- Around line 81-100: The return type currently reintroduces dependent keys via
Pick<Partial<FormOptions<...>>, FormOptionsDependentKeys & keyof TOptions>,
which forces those keys to be optional; replace that Partial+Pick overlay with a
mapped type that preserves the original optionality from TOptions while mapping
each key to the resolved callback type from FormOptions. Concretely, instead of
Pick<Partial<...>, ...>, create a mapped overlay like: for each K in
FormOptionsDependentKeys & keyof TOptions produce a property K whose type is the
corresponding resolved FormOptions[K] but marked optional only when TOptions has
K as optional (use a conditional check like "undefined extends TOptions[K]" to
decide whether to append | undefined or make the property optional), and reuse
the existing FormOptions<...> instantiation to derive the resolved callback
types; update the signature in formOptions to use this new mapped overlay so
presence/optionality from TOptions is preserved.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b5cf28d1-a950-4b8d-b1f5-6ed4306220dc

📥 Commits

Reviewing files that changed from the base of the PR and between 6a73479 and 276dead.

📒 Files selected for processing (3)
  • .changeset/fix-form-options-validator-inference.md
  • packages/form-core/src/formOptions.ts
  • packages/form-core/tests/formOptions.test-d.ts

Comment on lines +81 to +100
): Omit<TOptions, FormOptionsDependentKeys> &
Pick<
Partial<
FormOptions<
NoInfer<TFormData>,
NoInfer<TOnMount>,
NoInfer<TOnChange>,
NoInfer<TOnChangeAsync>,
NoInfer<TOnBlur>,
NoInfer<TOnBlurAsync>,
NoInfer<TOnSubmit>,
NoInfer<TOnSubmitAsync>,
NoInfer<TOnDynamic>,
NoInfer<TOnDynamicAsync>,
NoInfer<TOnServer>,
NoInfer<TSubmitMeta>
>
>,
FormOptionsDependentKeys & keyof TOptions
> {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

In TypeScript, does Pick<Partial<{ onSubmit: () => void }>, "onSubmit">keeponSubmitoptional, and does{ onSubmit?: () => void }["onSubmit"]includeundefined?

💡 Result:

Yes on both points. 1) Pick<Partial<{ onSubmit: => void }>, "onSubmit"> keeps onSubmit optional. - Partial makes every property optional: it “set[s] … properties … to optional” [1]. - Pick preserves whether a property is optional in the source type because it “picks” those named properties from the given type [1]. 2) { onSubmit?: => void }["onSubmit"] includes undefined. - An optional property onSubmit?: ... is treated as having the possibility of being absent, so indexing its property type yields a union with undefined (optional implies “possibly undefined”). Practical equivalence: - Pick<Partial<{ onSubmit: => void }>, "onSubmit"> behaves like { onSubmit?: => void }. - Therefore, ({ onSubmit?: => void })["onSubmit"] is ( => void) | undefined.

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="packages/form-core/src/formOptions.ts"

# Show the relevant section around the reported lines
sed -n '1,140p' "$FILE" | cat -n

# Locate FormOptionsDependentKeys definition and any related helpers/usages
rg -n "FormOptionsDependentKeys" "$FILE"

Repository: TanStack/form

Length of output: 3988


Preserve presence/optionality of dependent callback keys in formOptions return type
formOptions reintroduces validators/onSubmit/onSubmitInvalid/listeners via Pick<Partial<FormOptions<...>>, ...>, and Partial makes those keys optional—so even when TOptions supplies them, the return type can widen them to ... | undefined (e.g. onSubmit?: ...).
Replace the Pick<Partial<...>> overlay with a mapped overlay that preserves optionality from TOptions while using the resolved callback types.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/form-core/src/formOptions.ts` around lines 81 - 100, The return type
currently reintroduces dependent keys via Pick<Partial<FormOptions<...>>,
FormOptionsDependentKeys & keyof TOptions>, which forces those keys to be
optional; replace that Partial+Pick overlay with a mapped type that preserves
the original optionality from TOptions while mapping each key to the resolved
callback type from FormOptions. Concretely, instead of Pick<Partial<...>, ...>,
create a mapped overlay like: for each K in FormOptionsDependentKeys & keyof
TOptions produce a property K whose type is the corresponding resolved
FormOptions[K] but marked optional only when TOptions has K as optional (use a
conditional check like "undefined extends TOptions[K]" to decide whether to
append | undefined or make the property optional), and reuse the existing
FormOptions<...> instantiation to derive the resolved callback types; update the
signature in formOptions to use this new mapped overlay so presence/optionality
from TOptions is preserved.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

any type in form validators when using formOptions

1 participant