Skip to content

bug: useForm and FormApi require 9 generic types instead of 1 starting 0.43.0 #1175

Closed

Description

@favna

Describe the bug

Previously with v0.42.x the types of useForm (react hook) and FormApi (core class) had 2 generic types, with only 1 being required:

  TFormData,
  TFormValidator extends Validator<TFormData, unknown> | undefined = undefined,

Starting 0.43.0 however this has changed to 9 required generic types:

  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>,
  TOnServer extends undefined | FormAsyncValidateOrFn<TFormData>,

Even if the form desired by the user does not have any validations they are now forced to define their strictly typed form like so:

const myForm = new FormApi<MyInterface, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined>

Which is extremely cumbersome. Furthermore, for our work project we also wrap a few Tanstack Form component with our custom Material UI components and we want those to be strictly typed, which would now become a complete flustercluck (excuse my french) with generic types.

I would really appreciate it if the defaults for the generic types could be re-introduced.

Your minimal, reproducible example

https://www.typescriptlang.org/play/?#code/JYWwDg9gTgLgBAbzgVwM4FMBi0RwL5wBmUEuA5AAIwCGAdqjQMYDWA9FOtYzALSE5kA3AChhwWjHRRCXdHACyATwAi1GomFwtcWtRDoAXHAZRxAcxHa41M4Z3IQAIyki8oxhHrwQi7FFwAvCgYfiAAPEqqNAB8ABQImtoAJugyyAA2MABq1OnI6KhGCVZWNnYAzAAMADSJJTp6dmSY1ABuumR1+MJ4AJRAA

Steps to reproduce

  1. Go to the above linked TypeScript playground
  2. Go to the "errors" tab
  3. See the error "Expected 9 type arguments, but got 1."

Code used on playground:

import { useForm } from '@tanstack/react-form';

interface MyData {
    name: string;
    age: number;
}

const myForm = useForm<MyData>({
    defaultValues: {
        age: 30,
        name: 'Favna'
    }
})

Expected behavior

Expected behaviour is that the code above works just fine, regardless of the 8 missing generic types.

How often does this bug happen?

Every time

Screenshots or Videos

No response

Platform

  • OS: MacOS
  • Version: Seqouia 15.3.1

TanStack Form adapter

react-form

TanStack Form version

0.43.2

TypeScript version

5.7.3

Additional context

No response

Activity

  1. crutchcorn commented on Feb 24, 2025

    @crutchcorn
    Contributor

    This is correct behavior and is required for many of our TS types to work properly. PRs welcomed if you can solve our types another way.

  2. crutchcorn commented on Feb 24, 2025

    @crutchcorn
    Contributor

    We never intend anyone to pass anything to useForm's generics. Instead, you should be doing:

    const myForm = useForm({
        defaultValues: {
            age: 30,
            name: 'Favna'
        } as MyData
    })
  3. VibrantMoose commented on Feb 24, 2025

    @VibrantMoose

    In my case I don't pass generics to the hook, but I pass the myForm instance to react child components. I was using ReactFormExtendedApi in the children's props interfaces. I have multiple huge forms I had to split in multiple components, so the useForm hook is called in a parent component. Am I doing this wrong ?

    EDIT: Oh.... I missed the useField hook I guess ?

  4. crutchcorn commented on Feb 24, 2025

    @crutchcorn
    Contributor

    Do not use the useField hook, it's strongly suggested against using in your own codebase.

    We do not currently have a good alternative API to do what you're looking for, though I have an API in mind for how to do this. However, it's unclear to me some of the edgecases.

    EG:

    function Parent() {
        const form = useForm({defaultValues: {firstName: ""} });
    
        return <Child form={form} />
    }
    
    // Imagine a new `withForm` HOC API that handles typings for you
    const Child = withForm(({form}) => {
        // How do we ensure that `name` is not invalid? How do we infer the type of `field.handleChange` if we allow that?
        return <form.Field name="testing"/>
    });
  5. favna commented on Feb 24, 2025

    @favna
    Author
  6. crutchcorn commented on Feb 24, 2025

    @crutchcorn
  7. deniskabana commented on Feb 25, 2025

    @deniskabana

    @crutchcorn This is a very tough decision that you made for us.

    Unfortunately, I second the OP. We never have any objects representing the data, as ALL of these in our entire app are received asynchronously during run-time. We however have full access to the interfaces that the objects adhere to during compile-time and for all TypeScript features our project HAS to rely on generics.

    This decision seems very short-sighted. Required code in 0.43.x:

    /* eslint-disable @typescript-eslint/no-explicit-any */
    
    export type CReactFormApi<TFormData extends object> = ReactFormApi<
      TFormData,
      any,
      any,
      any,
      any,
      any,
      any,
      any,
      any,
      any
    >;
    
    export type CReactFormExtendedApi<TFormData extends object> = ReactFormExtendedApi<
      TFormData,
      any,
      any,
      any,
      any,
      any,
      any,
      any,
      any,
      any
    >;

    OR

    const form = useForm({ defaultValues: {} as IFormSchema }); // This breaks TS and is error-prone

    Can this issue be re-wised or re-visited please? Specifically for those of us who have no defaultValues when compiling TS?

  8. crutchcorn commented on Feb 25, 2025

    @crutchcorn
    Contributor

    This decision is the opposite of short-sighted; it's motivated by seeing our consistent shifts of generics and TS code in general. If we encouraged folks to use our generics, they'd break in a minor easily, even past v1.

    Our goal is to create APIs that handle this usecase without the need of generics passing of any kind.

    To that end, please review the APIs outlined in this newest PR, which I believe should handle the usage you need:

    #1179

  9. deniskabana commented on Feb 25, 2025

    @deniskabana

    Thank you for your response @crutchcorn. This does not address the lacking JS representation of any data during compile-time.

    This code is from your PR's description:

    // /src/features/people/shared-form.ts, to be used across `people` features
    const formOpts = formOptions({
      defaultValues: {
        firstName: 'John',
        lastName: 'Doe',
      },
    })

    We only have TS interfaces, no objects that would represent them run-time. These are constructed from the form itself the first time it is submitted. Until then, all we have is a TS interface we import. This makes it close to impossible to stick with @tanstack/form-react for multiple projects I am working on.


    In 0.42.x, this is what our code looked like:

    const Test = () => {
      const form = useForm<IFormSchema>({ onSubmit: console.log });
    
      return (
        <FormInputText /* custom abstraction */
          form={form}
          name="locales.default" /* TS suggestions */
          label="Default language"
          minLength={2}
          maxLength={2}
          required
        />
      )
    };
  10. crutchcorn commented on Feb 25, 2025

    @crutchcorn
    Contributor

    @deniskabana I'm not following why the following does not work:

    const formOpts = formOptions({ defaultValues: { } as SomeInterfaceHere })

    It works at compile time just fine.

  11. deniskabana commented on Feb 25, 2025

    @deniskabana

    @crutchcorn This causes unexpected failures that TS does not prevent:

      const form = useForm({
        defaultValues: {} as MySchema,
      });
    
      // Following code is valid TS, no errors
    
      const defaultLocale = useStore(form.store, (state) => state.values.locales.default);
      // [JS] TypeError: Cannot read properties of undefined (reading 'default')
      console.log(defaultLocale.length);
      // [JS] TypeError: Cannot read properties of undefined (reading 'length')

    EDIT: This causes the TS to not be useful at all since it never flagged the code as incorrect.

  12. crutchcorn commented on Feb 25, 2025

    @crutchcorn
    Contributor

    That's a bug, and one I'm rather surprised to see given how extensive our type tests are for this usecase.

    Can you create a minimal repro on an issue and I'll prioritize a fix/response?

  13. deniskabana commented on Feb 25, 2025

    @deniskabana

    I'm not sure what the bug is here, as it is an expected TypeScript behavior with typecasting. Try this for yourself in TS:

    (({}) as { firstName: string }).firstName.trim();
    // [TS] ✅
    // [JS] TypeError: Cannot read properties of undefined (reading 'trim')
    ({}).firstName.trim();
    // [TS] Cannot find name 'firstName'.
    // [JS] TypeError: Cannot read properties of undefined (reading 'trim')

    This is why I keep pushing against your notion of not needing the generic types - typecasting foregoes any information about the shape of the object by design.

    Edit: CC @crutchcorn

  14. crutchcorn commented on Feb 25, 2025

    @crutchcorn
    Contributor

    I'm fundamentally not understanding how a generic type fixes this.

    Let's assume that you pass a generic as it was before and we """"fixed"""" this behavior differencial:

    useForm<MySchema>();

    vs

    useForm({defaultValues: {} as MySchema});

    Both of these values will behave identically at runtime:

    // Following code is valid TS, no errors
    
    const defaultLocale = useStore(form.store, (state) => state.values.locales.default);
    // [JS] TypeError: Cannot read properties of undefined (reading 'default')
    console.log(defaultLocale.length);
    // [JS] TypeError: Cannot read properties of undefined (reading 'length')

    The behavior is exactly the same at both compile time and runtime alike. The only difference is in DX and one of our core philosophies (even from the start) has been to avoid anyone passing anything to our generics.


    Even from day 1 we always guided people away from using generics in our Discord, issues, and forums. For a short while, we had a few docs/test items that used this API, but they were errant and removed as soon as we caught them during reviews.

    Again, if someone is able to make it such that you can pass a single generic and have all the inferencing still work we'd love a PR.

    That said, it's not currently possible in TypeScript - it's an inherent limitation on the language:

    function test<TExplicit = never, TInfer = undefined>(val: TInfer) {
        return val;
    }
    
    const val = test(123);
    //      ^? `123`
    
    const castVal = test<never>(123);
    //                          ^? Argument of type '123' is not assignable to parameter of type 'undefined'.(2345)

    And again, it's incredibly brittle in case we ever add a mandatory type in the future.


    I understand y'all are frustrated, I empathize with this. Could I go back in time and make our boundaries and guidance more clear I would. Hindsight is 20/20. But for 99% of our users, I strongly believe this is the right way to go.

  15. KGSands commented on Feb 25, 2025

    @KGSands

    Previously for nested forms I would do the following e.g.:

    const groupForm = useForm<
            {basic: SomePayload},
            Validator<unknown, ZodType>
        >({
            defaultValues: {
                basic: editPayload ? editPayload : DEFAULT_EMPTY_PAYLOAD,
            },
        });
    

    And in the child prop:

        form: FormType<{basic: SomePayload}>;
    

    where FormType was:

    export type FormType<T> = FormApi<T, Validator<unknown, ZodType>> &
        ReactFormApi<T, Validator<unknown, ZodType>>;
    

    I guess I can get around the typing with:

    const groupForm = useForm({
            defaultValues: {
                basic: editPayload ? editPayload : DEFAULT_EMPTY_PAYLOAD,
            },
        });
    

    But what is the child type now? I'm not sure.

    edit: perhaps I have to wait on #1179

  16. blipk commented on Mar 1, 2025

    @blipk

    I had no issue storing the useForm result in tanstack store and using it in all my components

  17. crutchcorn commented on Mar 1, 2025

    @crutchcorn
    Contributor

    I had no issue storing the useForm result in tanstack store and using it in all my components

    Then continue doing so, just know that it's not an officially supported mechanism for Form, as you lose much of what makes TSF great.

    Instead we suggest this:

    https://tanstack.com/form/latest/docs/framework/react/guides/form-composition

  18. onehorsetown commented on Mar 24, 2025

    @onehorsetown

    Running into this issue also. I "simply" want to be able to properly type of the return type of useForm at compile time.

    const form = useForm({ ... });

    What is the type of form? I understand that the withForm HOC provides a mechanism to type the form instance in a Component's child render function. But what I need is to infer the form's type not as a component prop, but as something I want to pass to a non-react utility function or my own React Context.

    const doSomethingOutsideOfReact = (form: ???, ...) => ...

    How to solve this? Well suffice it to say that entire Tanstack inferred typing is both truly amazing, but also well beyond my ability to comprehend. In TanStack router we know that it involves some black magic involving augmenting the RouterConfig type and in tRPC (not tanstack, but equally awesome inference), it involves the utility types inferRouterInputs<T> and inferRouterOutputs<T>.

    https://trpc.io/docs/client/vanilla/infer-types

    So perhaps I am asking for:

    type FormType = inferFormType<Schema>;
    
    const doSomethingOutsideOfReact = (form: FormType, ...) => ...
    

    I understand that FormApi takes 9/10 generic arguments for a reason so the above is likely wishful thinking.

  19. MinnDevelopment commented on Mar 25, 2025

    @MinnDevelopment

    We use ReturnType of our custom form hook initializer function as the form's type, this works if you don't require any dynamic default values and validators.

    const { useAppForm } = createFormHook({ ... });
    
    export function useMyForm() {
      return useAppForm({ ... });
    }
    
    export type MyForm = ReturnType<typeof useMyForm>;
    
    // Use as prop or function parameter without any issues:
    interface MyProps {
      form: MyForm;
    }
  20. craigbehnke commented on Apr 27, 2025

    @craigbehnke

    I just tried to upgrade, and was blown away by this change. (Not in a good way)

    We have a custom react hook that wraps the tanstack useForm hook and then adds some additional functions and a FormWrapper component to the final returned type. This lets us centralize all form boilerplate into one hook that can then be imported into a bunch of different forms within the app.

    This change makes that untenable, since my understanding is that we would have to add all the new generics to the hook itself and then manually set those generics at every call site.

    I don't want to have to do this (it would take a long time to achieve, if it is even possible), and so I checked the npm registry for the last version of the library before this change, and it seems that version is the 3rd most popular over the past week.

    Version Downloads (Last 7 Days) Published
    1.3.0 31,548 23 days ago
    1.6.3 24,270 8 days ago
    0.42.1 17,896 2 months ago
    0.33.0 14,636 7 months ago
    1.3.1 7,658 18 days ago
    1.3.3 5,433 15 days ago

    Evidently a lot of people, myself included, do not want to deal with the headaches this change introduces. Is there a way to make this change less absurd? Put another way specifically for my use case, is there a way to wrap the useForm hook in another hook and have total type safety, just like before?

    Something like:

    interface InputProperties<...> extends TanStackFormOptions<...> {
        // Extra options here
    }
    
    interface ReturnValues<...> extends TanStackFormApi<...> {
        // Extra returned values here
    }
    
    export function useCustomForm<...>(properties: InputProperties<...>): ReturnValues<...> {
        const form = useForm(properties)
        
        // Extra code here
    
        return {
            ...form,
            // Extra return values
        }
    }

    Is that possible right now, and if not, what would need to change?

  21. crutchcorn commented on Apr 27, 2025

    @crutchcorn
    Contributor

    Hey @craigbehnke , I can see you're frustrated. Here's the deal: I'm frustrated too. Your tone isn't appropriate and frankly your feedback is months late.

    Please feel free to reopen a discussion elsewhere with a more inquisitive and polite response and I might reply.

    Until then, please consider the folks on the maintaining side.

    Have a good day!

  22. craigbehnke commented on Apr 27, 2025

    @craigbehnke

    @crutchcorn I'm not sure what in my comment warranted your snarky response.

    Previously valid behaviors using the library broke without a workaround or migration path -- that's not something I'm used to in any tanstack library, and it frankly surprised me.

    I went over what I have been doing up until now that was impacted by the change.

    I highlighted that this is not an isolated issue, but affects many people that have not been able to update.

    I then outlined what I wanted to be able to do and asked whether it was possible to a thread that, to be candid, was already discussing workarounds for the issue.

    There was no language, and I did not personally insult you.

    Please forgive my frustration that I had to deal with a breaking change raised by an automated branch on a Sunday.

  23. craigbehnke commented on Apr 28, 2025

    @craigbehnke

    @crutchcorn On further reflection, I apologize for any perceived slight against you or the project. My frustration is with the work suddenly on my plate, not you, and I recognize that you are likely working on this project on a volunteer basis.

    With that in mind, I would greatly appreciate any input you may have into whether there is a migration path from the custom hook I outlined above to the current best practice for using the library. How can we get a fully typed form object out of the useForm hook (and a fully typed input object) without manually setting the 8-9 new generic types, which you have already said is not the intended use case?

  24. crutchcorn commented on Apr 28, 2025

    @crutchcorn
    Contributor

    @craigbehnke I'm both surprised and grateful that you've come back to this issue in such a positive light. I do appreciate it ✨ I'm also sorry you've gotten sudden work on your plate - that's genuinely never fun.

    So yeah, here's the reason for the 9 generics: They're inferring things from the code you'll likely want to keep with any kind of wrapper around useForm that you'd make. This includes:

    • Types of onChange/onBlur/et al errors
    • defaultValues as FormData
    • Likely more generics added in future releases for other features

    The way I'd work around this actually pretty heavily depends on what your goals are with useCustomForm. What are you looking to extend from form with?

    I actually have an idea for how to add useCustomForm more easily, but worry that we may have already solved this with useAppForm and createFormHook APIs:

    https://tanstack.com/form/latest/docs/framework/react/guides/form-composition

    Could you look through those docs real quick and if they don't suit your needs let me know what you're aiming to do?

    BTW, as a quick "let's unblock Craig ASAP", you can just cast the other 9 generics as any and allow for implicit returns:

    import {useForm, FormOptions as TanStackFormOptions} from "@tanstack/react-form";
    
    interface InputProperties<TData> extends TanStackFormOptions<TData, any, any, any, any, any, any, any, any> {
        // Extra options here
    }
    
    // Infer the return type
    export function useCustomForm<TData>(properties: InputProperties<TData>) {
        const form = useForm(properties)
    
        // Extra code here
    
        return {
            ...form,
            // Extra return values
       }
    } 

    Just be aware that this is a brittle solution that we can't respect as part of semantic versioning (we don't cover generics types under semver) so let's try to figure out a long-term solution together as well :)

  25. CanRau commented on Apr 28, 2025

    @CanRau

    Haven't followed this discussion closely and might be a little off here, doesn't something like what I'm doing here work?

    FormState<
    	TFormData,
    	FormValidateOrFn<TFormData> | undefined,
    	FormValidateOrFn<TFormData> | undefined,
    	FormAsyncValidateOrFn<TFormData> | undefined,
    	FormValidateOrFn<TFormData> | undefined,
    	FormAsyncValidateOrFn<TFormData> | undefined,
    	FormValidateOrFn<TFormData> | undefined,
    	FormAsyncValidateOrFn<TFormData> | undefined,
    	FormAsyncValidateOrFn<TFormData> | undefined
    >;
  26. chce commented on Jul 4, 2025

    @chce

    I'm not sure what the bug is here, as it is an expected TypeScript behavior with typecasting. Try this for yourself in TS:

    (({}) as { firstName: string }).firstName.trim();
    // [TS] ✅
    // [JS] TypeError: Cannot read properties of undefined (reading 'trim')
    ({}).firstName.trim();
    // [TS] Cannot find name 'firstName'.
    // [JS] TypeError: Cannot read properties of undefined (reading 'trim')
    This is why I keep pushing against your notion of not needing the generic types - typecasting foregoes any information about the shape of the object by design.

    Edit: CC @crutchcorn

    This can be avoided by using satisfies. E.g. defaultValues: { name: {} satisfies Name as Name }

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions