form icon indicating copy to clipboard operation
form copied to clipboard

"Type instantiation is excessively deep and possibly infinite" and tsc crashing

Open fl0ydj opened this issue 1 year ago • 6 comments

Describe the bug

First off, thanks for the awesome library!

We have been observing some weird instances of "Type instantiation is excessively deep and possibly infinite". For some files, tsc is even crashing.

Our repo is private so can't share it but we created a couple of custom Fields/components, so maybe you can spot where this is coming from:

  1. We have been using zod as a form validator on Fields
  2. We wrap all forms in a form context provider to avoid prop drilling and are passing in reference from useRef(form) as we had some problems with stability in earlier versions (not sure if this still makes any difference).
Code
import type { MutableRefObject } from "react";
import { createContext, useContext } from "react";
import type { useForm } from "@tanstack/react-form";

export const FormContext: React.Context<MutableRefObject<ReturnType<typeof useForm<any>>> | undefined> =
  createContext<MutableRefObject<ReturnType<typeof useForm<any>>> | undefined>(undefined);

export function useFormContext<V>(): ReturnType<typeof useForm<V>> {
  const form = useContext(FormContext) as MutableRefObject<ReturnType<typeof useForm<V>>> | undefined;
  if (!form) {
    throw new Error("useFormContext must be used within a FormProvider");
  }
  return form.current;
}
  1. We are using custom fields like in the code below
Code

type IsDeepValueString<T, K extends DeepKeys<T>> = DeepValue<T, K> extends string | number | typeof Any
  ? K
  : never;

type StringDeepKeys<T> = {
  [P in DeepKeys<T>]: IsDeepValueString<T, P>;
}[DeepKeys<T>];

export const TextField = <V, N extends DeepKeys<V> & StringDeepKeys<V>>(
  props: {
    name: N;
    label?: string;
    regexPattern?: string;
    rightElement?: ReactNode;
    isPassword?: boolean;
    extraFieldProps?: Omit<FieldOptions<V, N, any>, "name">;
  } & Omit<InputProps, "name" | "label">,
): ReactNode => {
  const form = useFormContext<V>();
  const { name, label, regexPattern, rightElement, isPassword, ...rest } = props;

  return (
    <form.Field {...props.extraFieldProps} name={props.name}>
      {(field) => {
        console.log("ruleAsset", props.name, field.getValue());

        return (
          <FormControl isInvalid={field.state.meta.isTouched && field.state.meta.errors?.length > 0}>
            {props.label && <FormLabel htmlFor={String(props.name)}>{props.label}</FormLabel>}
            <InputGroup>
              <Input
                type={props.isPassword ? "password" : "text"}
                value={field.getValue() === Any ? "∞" : (field.getValue() as string)}
                name={String(field.name)}
                onChange={(e) => {
                  console.log("ruleasset ruleamount change", e.target.value);
                  const regex = new RegExp(props.regexPattern ?? "");
                  console.log("1");

                  if (e.target.value === "" || !props.regexPattern || regex.test(e.target.value)) {
                    console.log("2");

                    //need to cast as we extend string so its not sure it can be assinged properly
                    field.handleChange(e.target.value as DeepValue<V, N>);
                    console.log("2.1");
                  } else if (e.target.value === "∞") {
                    field.handleChange(Any as DeepValue<V, N>);
                  }
                  console.log("3");
                }}
                onBlur={field.handleBlur}
                {...rest}
              />
              {props.rightElement !== undefined && <InputRightElement>{props.rightElement}</InputRightElement>}
            </InputGroup>
            <FormErrorMessage>
              {field.state.meta.isTouched && field.state.meta.errors?.join(", ")}
            </FormErrorMessage>
          </FormControl>
        );
      }}
    </form.Field>
  );
};
  1. We are using some fields where we receive the value from a query. We want to run validation on it and use it with other fields so we decided for the below structure:
Code
export const GasEstimateSimulation = ({
  gasField,
  metaData,
  userCoin,
  approval,
}: {
  gasField: FieldApi<RequestTransactionValues, FormField.Gas, any>;
  metaData: CoinsData | undefined;
  userCoin: UserCoin | undefined;
  approval: Approval<UserRequestType.RequestTransaction>;
}): ReactNode => {
  const currentWorkspace = useCurrentWorkspace();
  const assets = useAssets(currentWorkspace);

  const estimatedGas = useEstimateGas(getEstimateGasArgsFromApproval(approval));
  console.log("Estimate gas error", estimatedGas, getEstimateGasArgsFromApproval(approval));

  useEffect(() => {
    console.log("setting gas", estimatedGas.data);
    if (estimatedGas.data !== gasField.getValue()) {
      gasField.handleChange(estimatedGas.data);
    }
  }, [estimatedGas.data, gasField]);

  return (
    <TransactionFeeCardFromGasResult
      gas={estimatedGas}
      network={getNetworkFromApproval(approval)}
      userCoin={userCoin}
      metaData={metaData}
      // isLoading={!assets.isSuccess || !estimatedGas.isSuccess}
      fontSize="xs"
      // error={estimatedGas.error}
    />
  );
};

Your minimal, reproducible example

Steps to reproduce

See examples above

Expected behavior

Less performance issues with type checking

How often does this bug happen?

None

Screenshots or Videos

No response

Platform

macOs

TanStack Form adapter

None

TanStack Form version

0.26.4

TypeScript version

5.5.4

Additional context

We are using the zod form adaptor at the same version (0.26.4)

fl0ydj avatar Aug 07 '24 19:08 fl0ydj

Following

singhparshant avatar Aug 09 '24 12:08 singhparshant

First thing I'd try is to replace the temporary workaround ReturnType<typeof useForm<V>> with the newly exposed ReactFormApi<V>, in other similar cases this helped already with the type issue.

If that doesn't work, could you please reproduce the case on a stackblitz? You can fork one from the docs (e.g. https://tanstack.com/form/latest/docs/framework/react/examples/simple)


Speaking of custom components, we've been drafting a possible API and recommended pattern in #825 (component example in https://github.com/TanStack/form/pull/825/files#diff-b2e0803ded7363bf3432f160f7c7f6e9bf4f982ad13b08ecb4789c228774f956). Since you're doing something similar your feedback can help shaping the final version :)

Balastrong avatar Aug 11 '24 10:08 Balastrong

Changed it to ReactFormApi<V> & FormApi<V>. Didn't fix the issue though.

Have been trying to reproduce some of the issues on stackblitz. Have not been able to do so though as it just starts happening once the types are pretty complex. Found one issue, where I had slightly conflicting generics. i.e.:

enum MyEnum {
a,
b,
c
};
type SubEnumType=MyEnum.a|MyEnum.b;

type FormData=FieldValues1&FieldValues2;

type FieldValues1<N extends SubEnumType=SubEnumType>={
fieldVal1:N
}

type FieldValues2={
fieldVal1:MyEnum, //reusing this value here as my field 2 depends on it and consumes it with useStore
fieldVal2: string
}

Changing fieldVal1:MyEnum, to fieldVal1:SubEnumType, solved the error in that situation. I have tried reproducing this in the stackblitz but even this didn't work as presumably my test types were too simple.

Concluding, generally it seems that if the formdata types get too complex, its hard to avoid these ts issues?

Will also give it a couple more tries in the next couple of days

fl0ydj avatar Aug 11 '24 15:08 fl0ydj

I'm also facing this issue in the project where I'm transitioning from Formik to Tanstack Form so I will likewise try to create a reproducible Stackblitz. So far what I can say is that the error occurs on a function I wrote setFormFieldValue:

import type { ReactFormApi, DeepKeys, DeepValue, Updater, Validator, FormApi } from '@tanstack/react-form';

export function setFormFieldValue<TFormData, TField extends DeepKeys<TFormData>>(
  form: FormApi<TFormData, Validator<TFormData, unknown>> & ReactFormApi<TFormData, Validator<TFormData, unknown>>,
  field: TField,
  updater: Updater<DeepValue<TFormData, TField>>
) {
  form.setFieldValue(field, updater);
  form.setFieldMeta(field, {
    errors: [],
    errorMap: { onChange: false },
    isDirty: true,
    isTouched: true,
    isValidating: false,
    isPristine: false
  });
}

Which I call from an onChange to set the value of another field. I noticed that if I only use setFieldValue (without the additional meta) that our Testing Library tests fail because the proper errors aren't shown, coming from this component:

import type { DeepKeys, DeepValue, FieldApi, Validator } from '@tanstack/react-form';

interface FormErrorProps<
  TParentData,
  TName extends DeepKeys<TParentData>,
  TFieldValidator extends Validator<DeepValue<TParentData, TName>, unknown> | undefined = undefined,
  TFormValidator extends Validator<TParentData, unknown> | undefined = undefined,
  TData extends DeepValue<TParentData, TName> = DeepValue<TParentData, TName>
> {
  readonly field: FieldApi<TParentData, TName, TFieldValidator, TFormValidator, TData>;
  readonly 'data-qa'?: string;
}

export function FormError<
  TParentData,
  TName extends DeepKeys<TParentData>,
  TFieldValidator extends Validator<DeepValue<TParentData, TName>, unknown> | undefined = undefined,
  TFormValidator extends Validator<TParentData, unknown> | undefined = undefined,
  TData extends DeepValue<TParentData, TName> = DeepValue<TParentData, TName>
>({ field, 'data-qa': dataQa }: FormErrorProps<TParentData, TName, TFieldValidator, TFormValidator, TData>) {
  if (field.state.meta.isTouched && field.state.meta.errors.length) {
    const message = field.state.meta.errors.at(0);
    const stringMessage = typeof message === 'string' ? message : '';

    if (!stringMessage) return null;

    return <CustomFormNotification message={stringMessage} severity='error' data-qa={dataQa ?? 'form-error'} />;
  }

  return null;
}

The type of the form structure where I call this function is:

enum MyEnum {
  ONE = 'one',
  TWO = 'two'
}

interface MyFormType {
  type: MyEnum;
  amount: number;
  sheetNumber: string | null;
  id: string | null;
}

And it's called like so:

setFormFieldValue(form, 'amount', event.target.value === MyEnum.TWO ? 100 : 20);

With the form being:

  const form = useForm<MyFormType, Validator<MyFormType, unknown>>({
    defaultValues: {
      type: MyEnum.ONE,
      amount: 20,
      sheetNumber: null,
      id: props.id
    },
    onSubmit: ({ value, formApi }) => {
      if (value.type === MyEnum.TWO) {
        value.sheetNumber = null;
      }
      reactQueryHook.mutate(value);

      formApi.reset();
    },
    validatorAdapter: yupValidator()
  });

For the time being I added an @ts-ignore on the line that's giving a problem but I do hope this can get fixed some way.

If it matters, this is my system info:

  System:
    OS: macOS 14.6.1
    CPU: (16) x64 Intel(R) Core(TM) i9-9980HK CPU @ 2.40GHz
    Memory: 15.67 MB / 32.00 GB
    Shell: 5.9 - /bin/zsh
  Binaries:
    Node: 20.16.0 - /private/var/folders/nk/rds5rlv12dn8yxm9qx6mgv0h0000gn/T/xfs-ad6c1b10/node
    Yarn: 4.3.1 - /private/var/folders/nk/rds5rlv12dn8yxm9qx6mgv0h0000gn/T/xfs-ad6c1b10/yarn
    npm: 10.8.1 - ~/.volta/tools/image/node/20.16.0/bin/npm
    pnpm: 9.7.0 - ~/.volta/bin/pnpm

favna avatar Aug 20 '24 15:08 favna

I recreated the most minimal reproduction possible:

import { FormApi, useForm } from "@tanstack/react-form";

const register = <Data,>(form: FormApi<Data>) => {};

const App = () => {
  const form = useForm({
    defaultValues: {
      name: "",
      title: "",
    },
  });

  const x = register(form);

  return null;
};

export default App;

CodeSandbox

It happens in forms with 2 or more fields.

Spaubleit avatar Aug 27 '24 14:08 Spaubleit

In my case, this problem was caused by having an interface with a property type Moment. This problem still occurs in the latest version 0.32.0

DaliborHomola avatar Sep 07 '24 13:09 DaliborHomola

This is still happening on 0.34.4.

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

const register = <Data>(form: FormApi<Data>) => {};

const App = () => {
  const form = useForm({
    defaultValues: {
      name: '',
      title: [] as string[],
    },
  });

  const x = register(form);

  return null;
};

The problem is caused by adding as string[] to title: []. The type is necessary for the form.pushFieldValue('title', 'hello'); to work, otherwise there's a type error that 'hello' is not assignable to parameter of type never

alexgorbatchev avatar Nov 06 '24 02:11 alexgorbatchev

Can you give it a try with the version from #1016?

To use it in your projects use this install script:

pnpm add https://pkg.pr.new/@tanstack/react-form@1016

Balastrong avatar Nov 12 '24 22:11 Balastrong

Thanks for looking into it! Still occurring for me though.

fl0ydj avatar Nov 14 '24 10:11 fl0ydj

Can you share an example on a stackblitz so I can try to play with it?

Balastrong avatar Nov 14 '24 11:11 Balastrong

Can you share an example on a stackblitz so I can try to play with it?

Had problems reproducing it back then :( I guess it scales with type complexity. Does your fix solve the examples the others provided? If yes, will give it another try

fl0ydj avatar Nov 16 '24 22:11 fl0ydj

The simple example yes, I also added a test for the type here https://github.com/TanStack/form/pull/1016/files#diff-10b547d59824e5f1426565e2505783160a33492805eab90bd86f9d342088bde4R38

Balastrong avatar Nov 16 '24 22:11 Balastrong

I have also been running into this issue when trying to replace the forms in my app with tanstack/forms,

but I can't seem to replicate this in a stackbiz repo.

It's in a nextjs project. and nested pretty deep in the file structure. and then the form itself is also passed as a prop to a form component which displays the form. Same issue happens if it render the form in the same component as the useForm hook.

this isn't just when developing either, it blocks the build process.

as for where the component is located:

./pages/management/cases/[caseId].tsx
  -> renders: ./components/subpage/notes.tsx
       -> renders ./component/note-header.tsx
             -> renders ./features/management/store-note.tsx <- (useForm is defined here)
                   imports {noteSchema, TNoteSchema} from '../notes/note-schema.ts' (for the form).
                   -> render ./features/management/notes/notes-form.tsx <- uses the form.Field where the issue is occurring.

the schema is pretty basic:

import {z} from 'zod';

export const noteSchema = z.object({
     noteDate: z.string().date(),
     duration: z.number().gte(1, "This field is required").lte(15, "Max number is 15"),
     notes: z.string().min(1, "this field is required"),
     files: z.array(z.instanceof(File))
});
export type TNoteSchema = z.infer<typeof noteSchema>;

don't know if this helps at all, but not sure what else to provide without being able to recreate this in stackbiz.

ghost avatar Nov 19 '24 23:11 ghost

same in

@TanStack/form: 0.39.0
typescript: 5.7.2

code

const form = useForm({name: ""})
 const nameField = useField({ name: 'name', form });
```
```
TS2589: Type instantiation is excessively deep and possibly infinite.
```

xmsz-stu avatar Dec 04 '24 09:12 xmsz-stu

Faced the same problem, form version 0.39, typescript 5.7.2

lpkobamn avatar Dec 05 '24 14:12 lpkobamn

Hey all, I am sorry but unless you're reporting some new information - please avoid "SAME" or "Facing this problem as well" or whatnot.

We're aware of the issue, but aren't sure the best way to fix it other than suggest "do not use useField", which is generally our suggestion anyway.

crutchcorn avatar Dec 05 '24 14:12 crutchcorn

Hey all, I am sorry but unless you're reporting some new information - please avoid "SAME" or "Facing this problem as well" or whatnot.

We're aware of the issue, but aren't sure the best way to fix it other than suggest "do not use useField", which is generally our suggestion anyway.

I apologize for writing an uninformative comment. I ran into this problem when I tried to use <form.Field/>, when trying to output types, it returns a similar error as the creator of this issue.

Unfortunately, I will not be able to give my repository, since the project is large and it contains confidential data. I want to note that intellisense works again after I comment out the line with <form.Field/>


  type PriceType = "price_dealer" | "price_dealer_region" | "price_internet";

  const { data: cart } = useQuery(cartQueryOptions());
  const { data: payment_types } = useQuery(paymentTypesQueryOptions());
  const { data: shipping_methods } = useQuery(shippingMethodsQueryOptions());
  const { user } = Route.useRouteContext();

  const price_type = user.price_type as PriceType;

  const selected_items = cart.filter((item) => item.isChecked);
  const selected_sum = _.sumBy(selected_items, function (item) {
    if (!item.product[price_type]) {
      return 0;
    }
    return item.product[price_type] * item.quantity;
  });

  console.log(selected_sum);

  const form = useForm({
    defaultValues: {
      cart: selected_items,
      shipping_method: {
        id: shipping_methods[0].id,
        title: shipping_methods[0].title,
      },
      payment_type: {
        id: payment_types[0].id,
        title: payment_types[0].title,
      },
    },
  });
const cart: {
    id: number;
    product: {
        id: string;
        description: Types.Optional<string>;
        price_internet: Types.Optional<number>;
        price_dealer_region: Types.Optional<number>;
        price_dealer: Types.Optional<...>;
        ... 5 more ...;
        stock: {
            ...;
        }[] | null;
    };
    quantity: number;
    isChecked: boolean;
}[]

const payment_types: {
    id: string;
    title: string;
    name: string;
}[]

const shipping_methods: {
    id: string;
    title: string;
    name: string;
}[]

const user: {
    id: string;
    first_name: string | null;
    email: string | null;
    avatar: string | ({
        url: Types.Optional<string>;
    } & {
        id: string;
        storage: string;
        filename_disk: string | null;
        ... 19 more ...;
        focal_point_y: number | null;
    }) | null;
    ... 8 more ...;
    banned_brands: {
        ...;
    }[] | null;
}

lpkobamn avatar Dec 05 '24 14:12 lpkobamn

is there any news related to the solution of this problem?

lpkobamn avatar Dec 14 '24 20:12 lpkobamn

I believe this issue arises in typscript when the type system encounters a recursive type definition or an excessively complex type that exceeds its resolution limits. I've experienced a similar error while attempting to output certain types from the intellisense

epenflow avatar Jan 29 '25 13:01 epenflow

I believe I've fixed the outstanding issues with this TypeScript problem. If you re-run into this problem, please make a minimal reproduction and let me know in a new GH issue.

crutchcorn avatar Mar 01 '25 06:03 crutchcorn

Is there a commit we could look at @crutchcorn?

dan-gamble avatar Mar 01 '25 09:03 dan-gamble

@dan-gamble truthfully I don't know there's a single commit to point to as a fix for this. The strategies in preventing infinite instantiation is that we:

  • Check how deep some transforms go and eagerly opt out to prevent too large of objects from creating a "loop" (not a loop, but TS' depth detection thinks it is)
  • NoInfer more to prevent additional types from being added (Why we had to make TS 5.4 the minimum with the latest minor release)

crutchcorn avatar Mar 01 '25 10:03 crutchcorn

im on TS 5.9. im still facing this issue. Also I've noticed this issue is related to zod. When i replaced zodSchema with an object i dont get the error

  const form = useForm({
    defaultValues: zodSchema,
    onSubmit: ({ value }) => {
      console.log({ value });
    },
  });

HoaX7 avatar Oct 08 '25 18:10 HoaX7