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