I am trying to build a field array component in TypeScript that can work with more than one form. To do this, I am trying to use a generic to pass the form fields interface, but I can’t quite get the types right (it is working, but has type issues).
If I import my interface (that describes my form’s fields) and use it in place of T
everything works as expected, but with the generic I get some errors. Here’s what I have:
import type { Control, FieldErrors, FieldValues } from 'react-hook-form'
import { useFieldArray } from 'react-hook-form'
interface Props<T extends FieldValues> {
control: Control<T>
errors: FieldErrors<T>
}
export function Tags<T extends FieldValues>({
control,
errors,
}: Props<T>) {
const { fields, append, remove } = useFieldArray({
name: 'tags',
control,
keyName: 'key',
rules: { required: 'Please choose at least one tag' },
})
const handleTagChange = (tag: React.ChangeEvent<HTMLInputElement>) => {
const { name, value, checked } = tag.target
if (checked) {
append({ id: value, name })
} else {
const index = fields.findIndex((field) => field.id === value)
remove(index)
}
}
const tags = [
{ id: '1', name: 'tag1' },
{ id: '2', name: 'tag2' },
{ id: '3', name: 'tag3' },
]
return (
<>
<div role="group" aria-labelledby="policy-tags">
<p id="policy-tags">Tags</p>
<div>
{tags.map((tag) => (
<>
<label key={tag.id} htmlFor={tag.name}>
{tag.name}
</label>
<input
type="checkbox"
onChange={(e) => handleTagChange(e)}
key={tag.id}
name={tag.name}
value={tag.id}
checked={fields.some((field) => field.id === tag.id)}
/>
</>
))}
</div>
{errors?.tags?.root && <p>{errors.tags.root?.message || ''}</p>}
</div>
</>
)
}
And the implementation:
<Tags<IFormFields> control={control} errors={errors} />
I am seeing these errors:
The component functions as expected, I just can’t get the types right. Any ideas?
2
Answers
For the first issue, you must define the shape of your type and provide it to the
useFieldArray
hook as follows:doing this should also fix the second issue please try the above and post a reply with your update I’ll try to edit the answer accordingly.
You are very close!
The following changes are required to make TypeScript understand how the data is flowing:
useFieldArray
can be fed 3 type arguments;TFieldValues
,TFieldArrayName
andTKeyName
(see ts docs)As
useFieldArray
takes part as a field in your form, it requires name (TFieldArrayName
) to be part ofTFieldValues
.useFieldArray
tries to infer the form fields through thecontrol
type, but that ends up – via yourProps
– at the emptyFieldValues
, where'tags'
does not exists.So as @Moussa stated, you have to provide a partial form with only the necessary tag field.
The control property type must be overridden due to complex internal generic pass-through (caused by
_subjects.state
and_reset
properties, those can not matchT
toFieldValues
althoughT
extendsFieldValues
).By setting
TFieldValues
toPartial<T>
, thecontrol
type can safely be overridden usingControl<FieldValues>
Solution
With the above changes, the following piece of code should work:
Bonus
As
tags
should always be available as key in your form, you should replaceFieldValues
byPartialTagForm
inTags<T extends FieldValues>
.Note that
tags
then also must be available in yourIFormFields