form icon indicating copy to clipboard operation
form copied to clipboard

`any` type in form validators when using formOptions

Open pedro757 opened this issue 5 months ago β€’ 19 comments

Describe the bug

const formOpts = formOptions({
  defaultValues: {
    description: "",
  },
  validators: {
    onSubmit: data => { //// HERE data IS any
      console.log("validando", data); // OUTPUT: { value: { description: "" }, formApi: {...
    }
  }
});

Your minimal, reproducible example

. just copy the code above

Steps to reproduce

  1. Copy and paste this code in your editor, see the type of data
const formOpts = formOptions({
  defaultValues: {
    description: "",
  },
  validators: {
    onSubmit: data => { //// HERE data IS any
      console.log("validando", data); // OUTPUT: { value: { description: "" }, formApi: {...
    }
  }
});

Expected behavior

I expected to infer the type

How often does this bug happen?

Every time

Screenshots or Videos

Image

Platform

  • linux

TanStack Form adapter

react-form

TanStack Form version

v1.14.1

TypeScript version

v5.8.3

Additional context

Maybe related to https://github.com/TanStack/form/issues/1583

pedro757 avatar Jul 11 '25 19:07 pedro757

The formOptions function is a simple helper designed for creating reusable configurations.

A practical example is when you want to split a large form across multiple files. In that scenario, you can use formOptions to define a shared configuration that is then passed to other utilities, such as withForm.

https://tanstack.com/form/latest/docs/framework/react/guides/form-composition#breaking-big-forms-into-smaller-pieces

The type is loose export declare function formOptions<T extends Partial<FormOptions<any, any, any, any, any, any, any, any, any, any>>>(defaultOpts: T): T;

Since inferencing with withForm will break if this is changed.

kusiewicz avatar Jul 12 '25 13:07 kusiewicz

formOptions should generate the form options you give it. At the moment, it appears to be limited, which defeats its purpose.

I see that overriding values can be problematic, which thankfully is covered by unit tests.

The concern is:

const formOpts = formOptions({
    defaultValues: { firstName: '' } as Person,
    // if it infers Person here
    validators: ({ value }) => {
       return somethingElse(value)  
    }
})

const Component = withForm({
    ...formOpts,
    // then this would break
    defaultValues: { firstName: '', lastName: '' } as ExtendedPerson
})

LeCarbonator avatar Jul 13 '25 13:07 LeCarbonator

@kusiewicz as @LeCarbonator said, the problem is when you need to use formOptions like this:

const formOpts = formOptions({
  defaultValues: {
    description: "",
  },
});


  const form = useForm({
    ...formOpts,
    validators: {
      onSubmit: (data) => {    // HERE data IS any
         // 
      },
    },
  });

Right now, the type inference in form validators only works if you declare the defaultValues in the useForm itself, like this:

  const form = useForm({
  defaultValues: {
    description: "",
  },
    validators: {
      onSubmit: (data) => {    // TYPE IS INFERRED CORRECTLY
         // 
      },
    },
  });

pedro757 avatar Jul 14 '25 13:07 pedro757

I had a go trying to fix this but am coming up short :/

Changing formOption's type to this

export function formOptions<
  TFormData,
  TOnMount extends undefined | FormValidateOrFn<TFormData>,
  TOnChange extends undefined | FormValidateOrFn<TFormData>,
  TOnChangeAsync extends undefined | FormAsyncValidateOrFn<TFormData>,
  TOnBlur extends undefined | FormValidateOrFn<TFormData>,
  TOnBlurAsync extends undefined | FormAsyncValidateOrFn<TFormData>,
  TOnSubmit extends undefined | FormValidateOrFn<TFormData>,
  TOnSubmitAsync extends undefined | FormAsyncValidateOrFn<TFormData>,
  TOnDynamic extends undefined | FormValidateOrFn<TFormData>,
  TOnDynamicAsync extends undefined | FormAsyncValidateOrFn<TFormData>,
  TOnServer extends undefined | FormAsyncValidateOrFn<TFormData>,
  TSubmitMeta = never,
>(
  defaultOpts: Partial<
    FormOptions<
      TFormData,
      TOnMount,
      TOnChange,
      TOnChangeAsync,
      TOnBlur,
      TOnBlurAsync,
      TOnSubmit,
      TOnSubmitAsync,
      TOnDynamic,
      TOnDynamicAsync,
      TOnServer,
      TSubmitMeta
    >
  >,
): Partial<
  FormOptions<
    TFormData,
    TOnMount,
    TOnChange,
    TOnChangeAsync,
    TOnBlur,
    TOnBlurAsync,
    TOnSubmit,
    TOnSubmitAsync,
    TOnDynamic,
    TOnDynamicAsync,
    TOnServer,
    TSubmitMeta
  >
> {
  return defaultOpts
}

fixes the types but breaks spreading extra options in like so

useForm({
      ...formOpts,
      defaultValues: {
        firstName: 'John',
        lastName: 'Doe',
        age: 10,
      })

Should the docs be changed to communicate that validators and listeners shouldn't be added to formOptions? Or does anyone have any other ideas on what I could try to fix this

a-is-4-adam avatar Aug 06 '25 07:08 a-is-4-adam

@a-is-4-adam at least for overwriting with different values, it can be argued that it doesn't match what the formOptions generated. However, what happens if you try to change the generated options with your implementation?

  • FormOptions with only defaultValues, extend by adding validators
  • FormOptions with validators, extend by overwriting validators

LeCarbonator avatar Aug 06 '25 09:08 LeCarbonator

Looks like any validators can be added when only default values are added to formOptions are spread into useForm. When overwriting validators the specific validator, i.e onSubmit, has to conform to the same type as the original validator defined in the formOptions. Validators not defined in the formOptions can be defined in useForm without any problems.

I've put up a draft PR to show this working https://github.com/TanStack/form/pull/1679

a-is-4-adam avatar Aug 07 '25 00:08 a-is-4-adam

One question, with this #1679 we shouldn't use validators and onSubmit in formOptions(), right ? I'm using v1.19.1


  type RowsFormType = { id: string }[]

  const formOpts2 = formOptions({
    defaultValues: {
      rows: [] as RowsFormType,
    },
    onSubmit: data => {
      console.log(data);
    },
    validators: {
      onSubmit: (data: { value: { rows: RowsFormType } }) => { 
        if (data.value.rows.length < 2) {
          return "It should have at least one row";
        }
      },
    },
  });

function Afddd() {
  const form = useAppForm(formOpts2); /// ERRORS

}

This is the error

1. tsserver: Argument of type '{ defaultValues: { rows: RowsFormType; }; onSubmit: (data: { value: { rows: RowsFormType; }; formApi: FormApi<{ rows: RowsFormType; }, FormValidateOrFn<{ rows: RowsFormType; }> | undefined, ... 9 more ..., unknown>; meta: unknown; }) => void; validators: { ...; }; }' is not assignable to parameter of type 'FormOptions<{ rows: RowsFormType; }, FormValidateOrFn<{ rows: RowsFormType; }> | undefined, FormValidateOrFn<{ rows: RowsFormType; }> | undefined, ... 8 more ..., unknown>'.
     Types of property 'onSubmit' are incompatible.
       Type '(data: { value: { rows: RowsFormType; }; formApi: FormApi<{ rows: RowsFormType; }, FormValidateOrFn<{ rows: RowsFormType; }> | undefined, ... 9 more ..., unknown>; meta: unknown; }) => void' is not assignable to type '(props: { value: { rows: RowsFormType; }; formApi: FormApi<{ rows: RowsFormType; }, FormValidateOrFn<{ rows: RowsFormType; }> | undefined, ... 9 more ..., unknown>; meta: unknown; }) => any'.
         Types of parameters 'data' and 'props' are incompatible.
           Type '{ value: { rows: RowsFormType; }; formApi: import("/home/myuser/Documents/myproject/node_modules/@tanstack/form-core/dist/esm/FormApi").FormApi<{ rows: RowsFormType; }, import("/home/myuser/Documents/myproject/node_modules/@tanstack/form-core/dist/esm/FormApi").FormValidateOrFn<{ rows: RowsFormType; }> | undefined...' is not assignable to type '{ value: { rows: RowsFormType; }; formApi: import("/home/myuser/Documents/myproject/node_modules/@tanstack/form-core/dist/esm/FormApi").FormApi<{ rows: RowsFormType; }, import("/home/myuser/Documents/myproject/node_modules/@tanstack/form-core/dist/esm/FormApi").FormValidateOrFn<{ rows: RowsFormType; }> | undefined...'. Two different types with this name exist, but they are unrelated.
             The types of 'formApi.options.defaultState' are incompatible between these types.
               Type 'Partial<import("/home/myuser/Documents/myproject/node_modules/@tanstack/form-core/dist/esm/FormApi").FormState<{ rows: RowsFormType; }, import("/home/myuser/Documents/myproject/node_modules/@tanstack/form-core/dist/esm/FormApi").FormValidateOrFn<{ rows: RowsFormType; }> | undefined, ... 8 more ..., import("/home/p...' is not assignable to type 'Partial<import("/home/myuser/Documents/myproject/node_modules/@tanstack/form-core/dist/esm/FormApi").FormState<{ rows: RowsFormType; }, import("/home/myuser/Documents/myproject/node_modules/@tanstack/form-core/dist/esm/FormApi").FormValidateOrFn<{ rows: RowsFormType; }> | undefined, ... 8 more ..., import("/home/p...'. Two different types with this name exist, but they are unrelated.
                 Type 'Partial<import("/home/myuser/Documents/myproject/node_modules/@tanstack/form-core/dist/esm/FormApi").FormState<{ rows: RowsFormType; }, import("/home/myuser/Documents/myproject/node_modules/@tanstack/form-core/dist/esm/FormApi").FormValidateOrFn<{ rows: RowsFormType; }> | undefined, ... 8 more ..., import("/home/p...' is not assignable to type 'Partial<import("/home/myuser/Documents/myproject/node_modules/@tanstack/form-core/dist/esm/FormApi").FormState<{ rows: RowsFormType; }, import("/home/myuser/Documents/myproject/node_modules/@tanstack/form-core/dist/esm/FormApi").FormValidateOrFn<{ rows: RowsFormType; }> | undefined, ... 8 more ..., import("/home/p...'. Two different types with this name exist, but they are unrelated.
                   Types of property 'errorMap' are incompatible.
                     Type 'ValidationErrorMap<undefined, undefined, undefined, undefined, undefined, "It should have at least one row" | undefined, undefined, undefined, undefined, undefined> | undefined' is not assignable to type 'ValidationErrorMap<undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined> | undefined'.
                       Type 'ValidationErrorMap<undefined, undefined, undefined, undefined, undefined, "It should have at least one row" | undefined, undefined, undefined, undefined, undefined>' is not assignable to type 'ValidationErrorMap<undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined>'.
                         Type '"It should have at least one row" | undefined' is not assignable to type 'undefined'.
                           Type '"It should have at least one row"' is not assignable to type 'undefined'. [2345]

I think that if we are not supposed to use certain properties in formOptions we should remove them.

pedro757 avatar Aug 11 '25 13:08 pedro757

@a-is-4-adam @LeCarbonator see the error introduced in the latest version described in the previous message.

pedro757 avatar Aug 11 '25 14:08 pedro757

I updated to the latest version and still has not typed values in validators. So how it should look like?

Image

kusiewicz avatar Aug 11 '25 15:08 kusiewicz

Uh oh ... it looks like I was too eager with the merge there ... sorry about that! Will revert it in a bit. Looks like the unit tests in the PR didn't have the validator parameter, which breaks it.

LeCarbonator avatar Aug 11 '25 16:08 LeCarbonator

We've have a pr up for review so we should have a fix out shortly, it would be a great help if you guys could check it out. 🀘 #1687

harry-whorlow avatar Aug 12 '25 09:08 harry-whorlow

Hey there! I tried the last version and I get this new error running this code:

type RowsFormType = { id: string }[]
  const formOpts2 = formOptions({
    defaultValues: {
      rows: [] as RowsFormType,
    },
    onSubmit: data => {
      console.log(data);
    },
    validators: {
      onSubmit: (data: { value: { rows: RowsFormType } }) => {
        if (data.value.rows.length < 2) {
          return "It should have at least one row";
        }
      },
    },
  });

function Afddd() {
  const form = useAppForm(formOpts2);

  return (
    <form onSubmit={async e => {
      e.preventDefault();
      e.stopPropagation();
      await form.handleSubmit(e); /// ERRORS HERE
    }}>
      //
    </form>
  );
}

The error message is this:

tsserver: Argument of type 'FormEvent<HTMLFormElement>' is not assignable to parameter of type 'never'. [2345]

@harry-whorlow

pedro757 avatar Aug 14 '25 15:08 pedro757

Hi @pedro757, why are you passing the submission event to the handleSubmit handler?

This is the function signature for handle submit.

Image

The overrides prop is for submit meta, that you have not in your example defined. Hence why its type is never, and why it throws a Ts error

Hope this helps πŸ˜„

harry-whorlow avatar Aug 14 '25 15:08 harry-whorlow

Here's another one:

From the example in my previous comment, if I delete the type:

type RowsFormType = { id: string }[]
  const formOpts2 = formOptions({
    defaultValues: {
      rows: [] as RowsFormType,
    },
    onSubmit: data => {
      console.log(data);
    },
    validators: {
      onSubmit: (data) => {             //TYPE DELETED HERE
        if (data.value.rows.length < 2) {
          return "It should have at least one row";
        }
      },
    },
  });

function Afddd() {
  const form = useAppForm(formOpts2); /// NOW THIS ERRORS AGAIN

}
1. tsserver: Argument of type '{ defaultValues: { rows: RowsFormType; }; onSubmit: (data: { value: { rows: RowsFormType; }; formApi: FormApi<{ rows: RowsFormType; }, FormValidateOrFn<{ rows: RowsFormType; }> | undefined, ... 9 more ..., never>; meta: never; }) => void; validators: { ...; }; }' is not assignable to parameter of type 'FormOptions<{ rows: RowsFormType; }, FormValidateOrFn<{ rows: RowsFormType; }> | undefined, FormValidateOrFn<{ rows: RowsFormType; }> | undefined, ... 8 more ..., never>'.
     Types of property 'onSubmit' are incompatible.
       Type '(data: { value: { rows: RowsFormType; }; formApi: FormApi<{ rows: RowsFormType; }, FormValidateOrFn<{ rows: RowsFormType; }> | undefined, ... 9 more ..., never>; meta: never; }) => void' is not assignable to type '(props: { value: { rows: RowsFormType; }; formApi: FormApi<{ rows: RowsFormType; }, FormValidateOrFn<{ rows: RowsFormType; }> | undefined, ... 9 more ..., never>; meta: never; }) => any'.
         Types of parameters 'data' and 'props' are incompatible.
           Type '{ value: { rows: RowsFormType; }; formApi: import("/home/myuser/Documents/myproject/node_modules/@tanstack/form-core/dist/esm/FormApi").FormApi<{ rows: RowsFormType; }, import("/home/myuser/Documents/myproject/node_modules/@tanstack/form-core/dist/esm/FormApi").FormValidateOrFn<{ rows: RowsFormType; }> | undefined...' is not assignable to type '{ value: { rows: RowsFormType; }; formApi: import("/home/myuser/Documents/myproject/node_modules/@tanstack/form-core/dist/esm/FormApi").FormApi<{ rows: RowsFormType; }, import("/home/myuser/Documents/myproject/node_modules/@tanstack/form-core/dist/esm/FormApi").FormValidateOrFn<{ rows: RowsFormType; }> | undefined...'. Two different types with this name exist, but they are unrelated.
             The types of 'formApi.options.defaultState' are incompatible between these types.
               Type 'Partial<import("/home/myuser/Documents/myproject/node_modules/@tanstack/form-core/dist/esm/FormApi").FormState<{ rows: RowsFormType; }, import("/home/myuser/Documents/myproject/node_modules/@tanstack/form-core/dist/esm/FormApi").FormValidateOrFn<{ rows: RowsFormType; }> | undefined, ... 8 more ..., import("/home/p...' is not assignable to type 'Partial<import("/home/myuser/Documents/myproject/node_modules/@tanstack/form-core/dist/esm/FormApi").FormState<{ rows: RowsFormType; }, import("/home/myuser/Documents/myproject/node_modules/@tanstack/form-core/dist/esm/FormApi").FormValidateOrFn<{ rows: RowsFormType; }> | undefined, ... 8 more ..., import("/home/p...'. Two different types with this name exist, but they are unrelated.
                 Type 'Partial<import("/home/myuser/Documents/myproject/node_modules/@tanstack/form-core/dist/esm/FormApi").FormState<{ rows: RowsFormType; }, import("/home/myuser/Documents/myproject/node_modules/@tanstack/form-core/dist/esm/FormApi").FormValidateOrFn<{ rows: RowsFormType; }> | undefined, ... 8 more ..., import("/home/p...' is not assignable to type 'Partial<import("/home/myuser/Documents/myproject/node_modules/@tanstack/form-core/dist/esm/FormApi").FormState<{ rows: RowsFormType; }, import("/home/myuser/Documents/myproject/node_modules/@tanstack/form-core/dist/esm/FormApi").FormValidateOrFn<{ rows: RowsFormType; }> | undefined, ... 8 more ..., import("/home/p...'. Two different types with this name exist, but they are unrelated.
                   Types of property 'errorMap' are incompatible.
                     Type 'ValidationErrorMap<undefined, undefined, undefined, undefined, undefined, "It should have at least one row" | undefined, undefined, undefined, undefined, undefined> | undefined' is not assignable to type 'ValidationErrorMap<undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined> | undefined'.
                       Type 'ValidationErrorMap<undefined, undefined, undefined, undefined, undefined, "It should have at least one row" | undefined, undefined, undefined, undefined, undefined>' is not assignable to type 'ValidationErrorMap<undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined>'.
                         Type '"It should have at least one row" | undefined' is not assignable to type 'undefined'.
                           Type '"It should have at least one row"' is not assignable to type 'undefined'. [2345]

pedro757 avatar Aug 14 '25 15:08 pedro757

Hi @pedro757, why are you passing the submission event to the handleSubmit handler?

This is the function signature for handle submit.

Image The overrides prop is for submit meta, that you have not in your example defined. Hence why its type is never, and why it throws a Ts error

Hope this helps πŸ˜„

My bad, I got confused with other form libraries, I believe the previous comment highlights a real problem

pedro757 avatar Aug 14 '25 15:08 pedro757

Can throw your example into a code sandbox, that would be really helpful... it's hard to gather what’s going on without an example to see. πŸ˜„

harry-whorlow avatar Aug 14 '25 15:08 harry-whorlow

Can throw your example into a code sandbox, that would be really helpful... it's hard to gather what’s going on without an example to see. πŸ˜„

https://codesandbox.io/p/devbox/yfjvkd The important file to look at is client.tsx you'll see the type error there @harry-whorlow

pedro757 avatar Aug 14 '25 17:08 pedro757

Doesn't look like it's accessible. Did you make sure it's public? @pedro757

LeCarbonator avatar Aug 14 '25 17:08 LeCarbonator

Sorry, now it's accessible @LeCarbonator

pedro757 avatar Aug 14 '25 17:08 pedro757