reactjs - Problema com tipo genérico em um componente react-hook-form
Estou tentando construir um componente de array de campos em TypeScript que possa funcionar com mais de um formulário. Para fazer isso, estou tentando usar um genérico para passar a interface dos campos do formulário, mas não consigo acertar os tipos (está funcionando, mas tem problemas de tipo).
Se eu importar minha interface (que descreve os campos do meu formulário) e usá-la no lugar de T, tudo funciona como esperado, mas com o genérico eu recebo alguns erros. Aqui está o que eu tenho:
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>
</>
)
}
E a implementação:
<Tags<IFormFields> control={control} errors={errors} />
Estou vendo esses erros:
O componente funciona como esperado, só que não consigo acertar os tipos. Alguma ideia?
Resposta #1
Você está muito perto! As seguintes alterações são necessárias para fazer o TypeScript entender como os dados estão fluindo:
useFieldArraypode receber 3 argumentos de tipo;TFieldValues,TFieldArrayNameeTKeyName(veja a documentação do ts)
ComouseFieldArrayparticipa como um campo no seu formulário, ele requer que o nome (TFieldArrayName) seja parte deTFieldValues.useFieldArraytenta inferir os campos do formulário através do tipocontrol, mas isso acaba - via seuProps- emFieldValuesvazio, onde'tags'não existe.
Então, como @Moussa afirmou, você precisa fornecer um formulário parcial com apenas o campo de tag necessário.O tipo da propriedade control deve ser sobrescrito devido à passagem genérica interna complexa (causada pelas propriedades
_subjects.statee_reset, que não podem corresponderTaFieldValuesemboraTestendaFieldValues). Ao definirTFieldValuescomoPartial<T>, o tipocontrolpode ser sobrescrito com segurança usandoControl<FieldValues>
Solução
Com as alterações acima, o seguinte trecho de código deve funcionar:
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' },
})
Bônus
As tags devem sempre estar disponíveis como chave no seu formulário, você deve substituir FieldValues por PartialTagForm em Tags<T extends FieldValues>. Note que tags então também deve estar disponível no seu IFormFields
Resposta #2
Para o primeiro problema, você deve definir o formato do seu tipo e fornecê-lo ao hook useFieldArray da seguinte forma:
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,
});
Fazer isso também deve resolver o segundo problema. Por favor, tente o que foi sugerido acima e poste uma resposta com sua atualização. Vou tentar editar a resposta de acordo.

