Tipando hooks de formulário com React Hook Form
1. Introdução ao React Hook Form e TypeScript
React Hook Form é uma biblioteca poderosa para gerenciamento de formulários em React, e quando combinada com TypeScript, oferece uma experiência de desenvolvimento excepcional. A inferência de tipos permite detectar erros em tempo de compilação, como acessar campos inexistentes ou passar tipos incorretos para funções de validação.
Os benefícios são claros: autocomplete inteligente, redução de bugs runtime, e documentação viva através dos tipos. O ecossistema inclui três hooks principais: useForm para formulários completos, Controller para componentes controlados, e useFieldArray para arrays dinâmicos.
2. Tipagem básica com useForm
O primeiro passo é definir uma interface para os dados do formulário usando FieldValues:
import { useForm } from 'react-hook-form';
interface LoginForm {
email: string;
password: string;
rememberMe: boolean;
}
function LoginComponent() {
const {
register,
handleSubmit,
watch,
formState: { errors }
} = useForm<LoginForm>({
defaultValues: {
email: '',
password: '',
rememberMe: false
}
});
const onSubmit = (data: LoginForm) => {
console.log(data); // data é tipado como LoginForm
};
const watchedEmail = watch('email'); // string
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('email')} />
<input {...register('password')} />
<input type="checkbox" {...register('rememberMe')} />
<button type="submit">Login</button>
</form>
);
}
O genérico T em useForm<T>() garante que register, handleSubmit e watch operem apenas sobre campos válidos definidos na interface.
3. Tipagem de validação com esquemas (Zod / Yup)
A integração com resolvedores tipados eleva a segurança do formulário. Usando Zod, podemos inferir tipos automaticamente:
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
const userSchema = z.object({
name: z.string().min(3, 'Nome deve ter no mínimo 3 caracteres'),
age: z.number().min(18, 'Idade mínima é 18'),
email: z.string().email('Email inválido')
});
type UserFormData = z.infer<typeof userSchema>;
function UserForm() {
const {
register,
handleSubmit,
formState: { errors }
} = useForm<UserFormData>({
resolver: zodResolver(userSchema)
});
// errors é tipado como FieldErrors<UserFormData>
// errors.name?.message é string | undefined
// errors.age?.message é string | undefined
return (
<form onSubmit={handleSubmit((data) => console.log(data))}>
<input {...register('name')} />
{errors.name && <span>{errors.name.message}</span>}
<input type="number" {...register('age', { valueAsNumber: true })} />
{errors.age && <span>{errors.age.message}</span>}
<input {...register('email')} />
{errors.email && <span>{errors.email.message}</span>}
</form>
);
}
O tipo FieldErrors<T> mapeia automaticamente os erros de validação para cada campo do formulário.
4. Tipagem de campos aninhados e arrays dinâmicos
Formulários complexos frequentemente exigem objetos aninhados. O TypeScript permite tipar esses casos com precisão:
interface AddressForm {
user: {
name: string;
address: {
street: string;
city: string;
zipCode: string;
};
};
tags: string[];
}
const { register } = useForm<AddressForm>();
// Campos aninhados
register('user.address.street');
register('user.address.city');
// Arrays dinâmicos com useFieldArray
import { useFieldArray } from 'react-hook-form';
interface TagForm {
tags: { id: string; value: string }[];
}
function TagManager() {
const { control, register } = useForm<TagForm>({
defaultValues: { tags: [] }
});
const { fields, append, remove, update } = useFieldArray({
control,
name: 'tags'
});
// append aceita { id: string; value: string }
// update aceita (index: number, value: { id: string; value: string })
return (
<div>
{fields.map((field, index) => (
<div key={field.id}>
<input {...register(`tags.${index}.value`)} />
<button onClick={() => remove(index)}>Remover</button>
</div>
))}
<button onClick={() => append({ id: crypto.randomUUID(), value: '' })}>
Adicionar tag
</button>
</div>
);
}
5. Tipagem de componentes controlados com Controller
Componentes controlados exigem tipagem cuidadosa com ControllerRenderProps:
import { Controller, Control, FieldValues, Path } from 'react-hook-form';
interface ControlledInputProps<T extends FieldValues> {
name: Path<T>;
control: Control<T>;
label: string;
}
function ControlledInput<T extends FieldValues>({
name,
control,
label
}: ControlledInputProps<T>) {
return (
<Controller
name={name}
control={control}
render={({ field, fieldState }) => (
<div>
<label>{label}</label>
<input
value={field.value}
onChange={field.onChange}
onBlur={field.onBlur}
ref={field.ref}
/>
{fieldState.error && (
<span style={{ color: 'red' }}>{fieldState.error.message}</span>
)}
</div>
)}
/>
);
}
// Uso com tipagem estrita
interface ProfileForm {
name: string;
bio: string;
}
function ProfileEditor() {
const { control, handleSubmit } = useForm<ProfileForm>();
return (
<form onSubmit={handleSubmit((data) => console.log(data))}>
<ControlledInput name="name" control={control} label="Nome" />
<ControlledInput name="bio" control={control} label="Biografia" />
</form>
);
}
6. Tipagem de formulários assíncronos e submissão
Formulários que interagem com APIs exigem tratamento cuidadoso de erros:
interface ApiError {
code: string;
message: string;
field?: string;
}
interface AsyncFormData {
title: string;
content: string;
}
function AsyncForm() {
const {
handleSubmit,
setError,
formState: { isSubmitting, isValid }
} = useForm<AsyncFormData>({
mode: 'onChange'
});
const onSubmit = async (data: AsyncFormData): Promise<void> => {
try {
const response = await fetch('/api/posts', {
method: 'POST',
body: JSON.stringify(data)
});
if (!response.ok) {
const error: ApiError = await response.json();
if (error.field) {
setError(error.field as keyof AsyncFormData, {
type: 'manual',
message: error.message
});
} else {
setError('root.serverError', {
type: 'manual',
message: error.message
});
}
return;
}
// Sucesso
} catch (err) {
setError('root.serverError', {
type: 'manual',
message: 'Erro de conexão'
});
}
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('title', { required: true })} />
<textarea {...register('content', { required: true })} />
<button type="submit" disabled={isSubmitting || !isValid}>
{isSubmitting ? 'Enviando...' : 'Publicar'}
</button>
</form>
);
}
7. Boas práticas e padrões avançados
Criar hooks customizados tipados promove reutilização:
import { UseFormReturn, FieldValues, UseFormProps } from 'react-hook-form';
function useMyForm<T extends FieldValues>(
props?: UseFormProps<T>
): UseFormReturn<T> {
return useForm<T>({
mode: 'onBlur',
reValidateMode: 'onChange',
...props
});
}
// Reutilizando tipos entre formulário e API
interface UserDTO {
id: string;
name: string;
email: string;
}
type UserFormData = Pick<UserDTO, 'name' | 'email'>;
// Equivalente a: { name: string; email: string; }
// Evitando any com utilitários do TypeScript
type PartialUserForm = Partial<UserFormData>;
type UserWithOptionalEmail = Omit<UserFormData, 'email'> & {
email?: string;
};
8. Conclusão e próximos passos
A tipagem adequada de formulários com React Hook Form e TypeScript proporciona segurança, produtividade e manutenibilidade. Vimos como inferir tipos de esquemas de validação, trabalhar com campos aninhados, componentes controlados, e formulários assíncronos.
Os principais ganhos incluem: detecção precoce de erros, autocomplete inteligente, redução de testes manuais, e documentação viva através dos tipos. Para aprofundar, explore os recursos oficiais e a comunidade.
Referências
- React Hook Form - Documentação Oficial de TypeScript — Guia completo de tipagem com exemplos práticos e referência de tipos
- React Hook Form com Zod - Documentação do Resolvedor — Repositório oficial dos resolvedores com exemplos de integração Zod, Yup e outros
- Zod - Inferência de Tipos — Como usar
z.inferpara extrair tipos TypeScript de esquemas de validação - React Hook Form com useFieldArray - Exemplos Avançados — Documentação oficial com exemplos de tipagem para arrays dinâmicos
- TypeScript Utility Types para Formulários — Referência oficial sobre Partial, Pick, Omit e outros utilitários usados em formulários
- React Hook Form com Controller - Componentes Controlados — Documentação do Controller com tipagem de render props e field state
- Formulários Assíncronos com React Hook Form e TypeScript — Tutorial completo sobre validação assíncrona e tratamento de erros de API