skip to Main Content

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:

enter image description here

enter image description here

The component functions as expected, I just can’t get the types right. Any ideas?

2

Answers


  1. For the first issue, you must define the shape of your type and provide it to the useFieldArray hook as follows:

    interface FormValues {
      tags: { id: string; name: string }[];
    }
    
    const { fields, append, remove } = useFieldArray<FormValues>({
      name: 'tags' as const, // 'as const' asserts 'tags' as a literal type
      control,
    });
    

    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.

    Login or Signup to reply.
  2. 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 and TKeyName (see ts docs)
      As useFieldArray takes part as a field in your form, it requires name (TFieldArrayName) to be part of TFieldValues.
      useFieldArray tries to infer the form fields through the control type, but that ends up – via your Props – at the empty FieldValues, 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 match T to FieldValues although T extends FieldValues).
      By setting TFieldValues to Partial<T>, the control type can safely be overridden using Control<FieldValues>

    Solution

    With the above changes, the following piece of code should work:

    interface PartialTagForm {
      tags: { id: string; name: string }[]
    }
    
    export function Tags<T extends FieldValues>({ control, errors }: Props<T>) {
      const { fields, append, remove } = useFieldArray<
        Partial<PartialTagForm>,
        'tags',
        'key'
      >({
        name: 'tags',
        control: control as Control<FieldValues>,
        keyName: 'key',
        rules: { required: 'Please choose at least one tag' },
      })
    

    Bonus

    As tags should always be available as key in your form, you should replace FieldValues by PartialTagForm in Tags<T extends FieldValues>.
    Note that tags then also must be available in your IFormFields

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search