T
TanStack5mo ago
funny-blue

Type hints when declaring formComponent / fieldComponent

Is it possible to declare a fieldComponent / formComponent so that it is aware of the form / field it is used from? For example a Textfield component that error out if the field it is used in is not of type string Or an example I did recently: I wanted a generic ArrayField that displays a consistent field for array fields in form. I did it like this (look into message below) but it would be really nice to be able to not tie it to current form (in here FormData) but make it generic, preserving type safety for name prop
5 Replies
funny-blue
funny-blueOP5mo ago
formComponents: {
ArrayField<T extends keyof ArrayFields<FormData>>({
children,
name,
defaultValue,
delete_positioning,
}: {
children: (name: `${T}[${number}]`) => React.ReactElement;
name: T;
defaultValue: ArrayFields<FormData>[T][number];
} & VariantProps<typeof positioningVariants>) {
const form = useFormContext();
return (
<form.Field name={name as never} mode="array">
{(field) => (
<>
{(field.state.meta.errors?.length ?? 0) > 0 && (
<h3 className="text-destructive">
{`${field.state.meta.errors[0] as unknown as string}`}
</h3>
)}
{(field.state.value as unknown[])?.map((_, i) => (
<div className="grid grid-cols-[1fr_auto] gap-2" key={i}>
{children(`${name}[${i}]`)}
<Button
aria-label="remove item"
variant="ghost"
className={positioningVariants({ delete_positioning })}
size="icon"
onClick={() => (field.removeValue(i), field.handleBlur())}
>
<TrashIcon className="stroke-destructive text-2xl" />
</Button>
</div>
))}
<Button
aria-label="Add item"
variant="secondary"
className="w-full"
type="button"
onClick={() => (
field.pushValue(defaultValue as never), field.handleBlur()
)}
>
<PlusIcon className="size-7" />
</Button>
</>
)}
</form.Field>
);
},
},

type ArrayFields<F> = {
[K in DeepKeys<F> as DeepValue<F, K> extends unknown[]
? K
: never]: DeepValue<F, K>;
};
formComponents: {
ArrayField<T extends keyof ArrayFields<FormData>>({
children,
name,
defaultValue,
delete_positioning,
}: {
children: (name: `${T}[${number}]`) => React.ReactElement;
name: T;
defaultValue: ArrayFields<FormData>[T][number];
} & VariantProps<typeof positioningVariants>) {
const form = useFormContext();
return (
<form.Field name={name as never} mode="array">
{(field) => (
<>
{(field.state.meta.errors?.length ?? 0) > 0 && (
<h3 className="text-destructive">
{`${field.state.meta.errors[0] as unknown as string}`}
</h3>
)}
{(field.state.value as unknown[])?.map((_, i) => (
<div className="grid grid-cols-[1fr_auto] gap-2" key={i}>
{children(`${name}[${i}]`)}
<Button
aria-label="remove item"
variant="ghost"
className={positioningVariants({ delete_positioning })}
size="icon"
onClick={() => (field.removeValue(i), field.handleBlur())}
>
<TrashIcon className="stroke-destructive text-2xl" />
</Button>
</div>
))}
<Button
aria-label="Add item"
variant="secondary"
className="w-full"
type="button"
onClick={() => (
field.pushValue(defaultValue as never), field.handleBlur()
)}
>
<PlusIcon className="size-7" />
</Button>
</>
)}
</form.Field>
);
},
},

type ArrayFields<F> = {
[K in DeepKeys<F> as DeepValue<F, K> extends unknown[]
? K
: never]: DeepValue<F, K>;
};
adverse-sapphire
adverse-sapphire5mo ago
not with context. However, you can make the following structure:
// This field prop will only accept string
interface YourFieldComponentProps<T extends string> {
field: {
state: {
value: T
}
}
}
// This field prop will only accept string
interface YourFieldComponentProps<T extends string> {
field: {
state: {
value: T
}
}
}
Usage:
<form.AppField name="name">
{field => <field.YourComponent field={field}/>
</form.ApField>
<form.AppField name="name">
{field => <field.YourComponent field={field}/>
</form.ApField>
you can also extend this to something like a select option:
// This field prop will only accept string
interface YourFieldComponentProps<T extends string> {
field: {
state: {
value: T
}
},
// The value of this option. Add NoInfer so that the field dictates
// the type and not this property
value: NoInfer<T>
}
// This field prop will only accept string
interface YourFieldComponentProps<T extends string> {
field: {
state: {
value: T
}
},
// The value of this option. Add NoInfer so that the field dictates
// the type and not this property
value: NoInfer<T>
}
Usage
// Error! "Any string" is not applicable to "my" | "enum"
<form.AppField name="name">
{field => <field.YourComponent value="Any string" field={field}/>
</form.ApField>
// Error! "Any string" is not applicable to "my" | "enum"
<form.AppField name="name">
{field => <field.YourComponent value="Any string" field={field}/>
</form.ApField>
If you want as little extra characters as possible, you could also go for this, though I don't like it very much:
// This field prop will only accept string
interface YourFieldComponentProps<T extends string> {
state: {
value: T
}
}
}
// This field prop will only accept string
interface YourFieldComponentProps<T extends string> {
state: {
value: T
}
}
}
Usage:
<form.AppField name="name">
{field => <field.YourComponent {...field}/>
</form.ApField>
<form.AppField name="name">
{field => <field.YourComponent {...field}/>
</form.ApField>
One thing to note, this should only be used for inference. Use field context in the component, not this passed property
funny-blue
funny-blueOP5mo ago
Gotcha So I assume I should do a similar thing with the formComponent, right?
adverse-sapphire
adverse-sapphire5mo ago
maybe? I‘ve never tried for form components. If you have a use case example, I could perhaps suggest one way if the use case is some sort of subform (grouping multiple fields), that will unlikely have a good type safe solution.
funny-blue
funny-blueOP5mo ago
I pasted the use case above: abstracting the array field to have consistent delete / add new controls in every part of the form

Did you find this page helpful?