T
TanStack6mo ago
afraid-scarlet

Questions regarding migration to 1.x

Hello, I'm migrating from 0.35 to the latest. In my react app, I have a lot of forms that are passed as props so I usually need to type them. I used to do so with:
export type FormType<T> = FormApi<T, Validator<T>> &
ReactFormApi<T, Validator<T>>;
export type FormType<T> = FormApi<T, Validator<T>> &
ReactFormApi<T, Validator<T>>;
So that I could do:
type Props = {
form: FromType<MyFormType>
}

//Then stuff like form.Field, form.state.value, etc...
type Props = {
form: FromType<MyFormType>
}

//Then stuff like form.Field, form.state.value, etc...
But now that validators are gone and that FormApi seemingly changed, I'm struggling to find the correct way to get similar behavior. The closest I could find by sheer trial and error is:
export type FormType<T> = ReactFormExtendedApi<
T,
any,
any,
any,
any,
any,
any,
any,
any,
any
>;
export type FormType<T> = ReactFormExtendedApi<
T,
any,
any,
any,
any,
any,
any,
any,
any,
any
>;
but it seems to cause problems and might not be the best. For example if I want to have a component that can take any Form:
type Props = {
form: FormType<any>
}
type Props = {
form: FormType<any>
}
And then try to use it, I get:
Type 'ReactFormExtendedApi</*fields in my schema*/>' is not assignable to type 'FormType<any>'.
Type 'ReactFormExtendedApi</*fields in my schema*/>' is not assignable to type 'FormType<any>'.
The validator changes also seemed to break things, as I now get a type error whenever I have a form with default values and a schema with optional values, even when they should be/were compatible. Sorry if this is a bit rambly but since this is so new I've had little chance to find answers/examples online ,especially as some of the official examples seem down.
24 Replies
mute-gold
mute-gold6mo ago
My gut feeling says what you're looking for is the withForm HoC, that infers all the types for you. That way you don't have to define generics yourself. See example: https://tanstack.com/form/latest/docs/framework/react/guides/form-composition#breaking-big-forms-into-smaller-pieces So basically it's implemented as a HoC so that it can infer the form-types for you, instead of you having to define the form type using generics yourself.
afraid-scarlet
afraid-scarletOP6mo ago
Seems interesting, thanks, but I think that would mean rewriting a large portion of my app, i'm quite happy with my strongly typed generic so if I could just keep them it'd the best outcome.
rising-crimson
rising-crimson6mo ago
The generics have been completely rewritten in ~v0.42, so yes you will have to rewrite a bunch of stuff if you want to migrate to form composition API
afraid-scarlet
afraid-scarletOP6mo ago
Well I think buried under all of the new ones there is still one that represents a form, so then I only need to replace my old FormType with the new one in one place and I would be fine. My problem is that I don't think I have a good way to know which one replaced it.
rising-crimson
rising-crimson6mo ago
The generics have decent naming, it should give you an idea of which parameters are which. Most of the new generics are related to validation, so if none of your fields handle that, you can pass any. The main issue I see with keeping this passing structure is that you will unlikely be able to narrow the fields for type safety. There's also helper types (AnyFormApi and AnyFieldApi) that are as wide as they can be.
afraid-scarlet
afraid-scarletOP6mo ago
That is helpful thanks, do you know what generic hold the .Form and .Subscribe ? Or something that combines the api with those. There are so many generic that I'm kinda lost and just trying them randomly
rising-crimson
rising-crimson6mo ago
Sadly, I don't. I migrated from passing the Api directly as soon as the form composition beta came out
afraid-scarlet
afraid-scarletOP6mo ago
No problem, my best guess is still ReactFormExtendedApi, by typecasting I got around most of my problems. The only really annoying one left is the zod validation .
rising-crimson
rising-crimson6mo ago
zod v3.24.0 implements standard schema, so you should be alright with upgrading that
afraid-scarlet
afraid-scarletOP6mo ago
That might just be it lol, i'm on 3.23 ...
rising-crimson
rising-crimson6mo ago
I upgraded from 3.22 no problem, so I assume you're fine
afraid-scarlet
afraid-scarletOP6mo ago
Yup that was it thanks, now I only have a Type instantiation is excessively deep and possibly infinite.ts(2589) on one of my components that takes 3 deep keys as props, which I guess is too much now.
rising-crimson
rising-crimson6mo ago
it might just be an infer loop give NoInfer<T> a try
afraid-scarlet
afraid-scarletOP6mo ago
Not sure how I would use that, seems to not exist on the tanstack form package itself ? It seems to only find it from tanstack query/table
rising-crimson
rising-crimson6mo ago
it's from typescript. What version of ts are you using? tanstack v1.0 requires typescript v5.4+
afraid-scarlet
afraid-scarletOP6mo ago
I have 5.5.3
rising-crimson
rising-crimson6mo ago
yeah it's not imported then, it's part of typescript itself
afraid-scarlet
afraid-scarletOP6mo ago
I tried it on my DeepKey props to no avail, seems like a really cool thing to know though thanks A bad workaround I've found is typing the form as Form<any> but I lose the deepkeys autocomplete
rising-crimson
rising-crimson6mo ago
you have an excessively deep instantiation because A infers from B which infers from A which infers from B which infers from A ... it's the main issue with passing the generics directly as mentioned here
afraid-scarlet
afraid-scarletOP6mo ago
I would agree with you but it just happens in one use of the component, the others across the project being just fine and also NoInfer didn't fix it so might be something else I stand corrected, seems like it happens in other uses my bad
const form = useForm({
defaultValues: defaultDossierCreateValues,
validators: {
onChange: dossierCreateSchema,
},
}
const form = useForm({
defaultValues: defaultDossierCreateValues,
validators: {
onChange: dossierCreateSchema,
},
}
export const dossierCreateSchema = z.object({
PrioriteId: z.number().nullable(),
});

export const defaultDossierCreateValues = {
PrioriteId: null,
};
export const dossierCreateSchema = z.object({
PrioriteId: z.number().nullable(),
});

export const defaultDossierCreateValues = {
PrioriteId: null,
};
The types of 'input.PrioriteId' are incompatible between these types.
Type 'number | null' is not assignable to type 'number'.
Type 'null' is not assignable to type 'number'.ts(2322)
The types of 'input.PrioriteId' are incompatible between these types.
Type 'number | null' is not assignable to type 'number'.
Type 'null' is not assignable to type 'number'.ts(2322)
Most of these disappeared with the zod update, but some still remain, and I'm truly puzzled.
mute-gold
mute-gold6mo ago
It's likely because you're not explicitly typing the default values, so PrioriteId is being inferreed as being literally null (because without any external help TS has no way of knowing that PrioriteId also can be a number, so it's being evaluated to always being null) . You have to use zod.input to infer the type based on your schema. This should solve it:
export const defaultDossierCreateValues: z.input<typeof dossierCreateSchema> = {
PrioriteId: null,
};
export const defaultDossierCreateValues: z.input<typeof dossierCreateSchema> = {
PrioriteId: null,
};
afraid-scarlet
afraid-scarletOP6mo ago
Thanks, is there another way to explicitely type the form without going through default values ? Might come handy. I used to do it through the useForm hook, but that don't work anymore it seems
mute-gold
mute-gold6mo ago
The fields are inferred from default values, as far as I know. If you dont want default values (not sure why?) you could cast I guess:
const myForm = useForm({defaultValues: {} as z.input<typeof dossierCreateSchema>})
const myForm = useForm({defaultValues: {} as z.input<typeof dossierCreateSchema>})
But based on experience it's a good idea always use default values. You can also define this elsewhere using the formOptions function. Then just pass the returnvalue of that into your form. something like this:
const dossierCreateSchema = z.object({
PrioriteId: z.number().nullable(),
});

const defaultValues: z.input<typeof dossierCreateSchema> = { PrioriteId: null }
export const myFormOpts = formOptions({defaultValues, validators: { onChange: dossierCreateSchema } })

// and then in your component:

const myForm = useAppForm({...myFormOpts})
const dossierCreateSchema = z.object({
PrioriteId: z.number().nullable(),
});

const defaultValues: z.input<typeof dossierCreateSchema> = { PrioriteId: null }
export const myFormOpts = formOptions({defaultValues, validators: { onChange: dossierCreateSchema } })

// and then in your component:

const myForm = useAppForm({...myFormOpts})
afraid-scarlet
afraid-scarletOP6mo ago
Thanks

Did you find this page helpful?