Pular para o conteúdo
TypeScript

Tipar Formulário no React com TypeScript

Formulários sem tipagem viram armadilha silenciosa. Aprenda a tipar FormEvent, inputs controlados, state com interface e validação completa usando TypeScript no React.

Por que isso é importante

Tipar Formulário no React com TypeScript. Formulários sem tipagem viram armadilha silenciosa. Aprenda a tipar FormEvent, inputs controlados, state com interface e validação completa usando TypeScript no React.

O que muda ao tipar formulários no React

Galera, formulário sem tipagem é tipo dirigir sem painel: você não sabe a velocidade, não sabe o combustível, e quando bate, não sabe o que aconteceu. O TypeScript coloca todos os indicadores na sua frente.

A ideia central é simples: definir uma interface pro estado do formulário, tipar cada evento (onChange, onSubmit) e garantir que o dado que sai do form casa com o que a API espera receber. Dá pra fazer isso com formulários controlados nativos ou com bibliotecas como react-hook-form.

Passo a passo: tipando formulário do zero

  1. Crie a interface do estado do formulário — Defina cada campo com seu tipo exato: string, number, boolean. Isso vira o contrato do seu form.
  2. Tipar o useState com a interface — Use useState<FormData>() pra garantir que o state respeite a estrutura definida.
  3. Tipar o evento onChange — Use React.ChangeEvent<HTMLInputElement> pra cada input. Select e textarea têm tipos próprios.
  4. Tipar o evento onSubmit — Use React.FormEvent<HTMLFormElement> e chame e.preventDefault() com segurança total.
  5. Conectar com a API — O dado que sai do form já tá tipado. Passe direto pro fetch ou axios sem cast manual.

Exemplo prático: formulário de cadastro tipado

Veja como fica um formulário de cadastro com todos os tipos definidos. Repare que o TypeScript reclama na hora se você tentar acessar um campo que não existe na interface.

interface FormData {
  name: string;
  email: string;
  age: number;
}

function SignupForm() {
  const [form, setForm] = useState<FormData>({
    name: "",
    email: "",
    age: 0,
  });

  const handleChange = (
    e: React.ChangeEvent<HTMLInputElement>
  ) => {
    const { name, value, type } = e.target;
    setForm((prev) => ({
      ...prev,
      [name]: type === "number" ? Number(value) : value,
    }));
  };

  const handleSubmit = (
    e: React.FormEvent<HTMLFormElement>
  ) => {
    e.preventDefault();
    // form já está tipado como FormData
    console.log(form.name, form.email, form.age);
  };

  return (
    <form
      <input name="name" />
      <input name="email" />
      <input name="age" type="number" />
      <button type="submit">Cadastrar</button>
    </form>
  );
}

O ponto forte aqui: se você trocar o nome de um campo na interface, o TypeScript vai apontar todos os lugares que precisam de ajuste. Zero surpresa em runtime.

Tipando select, textarea e campos dinâmicos

Inputs de texto usam ChangeEvent, mas select e textarea pedem tipos diferentes. Misturar gera aquele erro clássico que ninguém entende de primeira.

// Select
const handleSelect = (
  e: React.ChangeEvent<HTMLSelectElement>
) => {
  setForm((prev) => ({ ...prev, role: e.target.value }));
};

// Textarea
const handleTextarea = (
  e: React.ChangeEvent<HTMLTextAreaElement>
) => {
  setForm((prev) => ({ ...prev, bio: e.target.value }));
};

// Handler genérico pra múltiplos tipos
type InputElements =
  | HTMLInputElement
  | HTMLSelectElement
  | HTMLTextAreaElement;

const handleAny = (
  e: React.ChangeEvent<InputElements>
) => {
  const { name, value } = e.target;
  setForm((prev) => ({ ...prev, [name]: value }));
};

Integração com react-hook-form e TypeScript

O react-hook-form já foi pensado pra TypeScript. Você passa a interface como generic pro useForm e pronto: register, errors, handleSubmit, tudo tipado automaticamente.

import { useForm } from "react-hook-form";

interface LoginForm {
  email: string;
  password: string;
}

function Login() {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm<LoginForm>();

  const LoginForm) => {
    // data.email e data.password 100% tipados
    console.log(data);
  };

  return (
    <form
      <input {...register("email", { required: true })} />
      {errors.email && <span>Email obrigatório</span>}

      <input
        type="password"
        {...register("password", { minLength: 6 })}
      />
      {errors.password && <span>Mínimo 6 chars</span>}

      <button type="submit">Entrar</button>
    </form>
  );
}

Se você tentar register("campo_inexistente"), o TypeScript barra na hora. Simples assim. Nada de descobrir campo errado só quando o usuário reclama.

Validação de tipos com Zod + react-hook-form

Pra quem quer validação no nível de runtime e compile-time ao mesmo tempo, Zod com react-hook-form é a combinação perfeita. O schema do Zod gera o tipo automaticamente.

import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";

const schema = z.object({
  name: z.string().min(2, "Nome muito curto"),
  email: z.string().email("Email inválido"),
  age: z.number().min(18, "Idade mínima 18"),
});

// Tipo gerado automaticamente do schema
type FormSchema = z.infer<typeof schema>;

function FormWithZod() {
  const { register, handleSubmit, formState: { errors } } =
    useForm<FormSchema>({
      resolver: zodResolver(schema),
    });

  const FormSchema) => {
    // Validado em runtime E tipado em compile-time
    console.log(data);
  };

  return (
    <form
      <input {...register("name")} />
      {errors.name && <p>{errors.name.message}</p>}
      <button type="submit">Enviar</button>
    </form>
  );
}

Erros comuns ao tipar formulários

Usar 'any' no evento: mata toda a segurança. Sempre tipar com React.FormEvent ou React.ChangeEvent.

Esquecer de tipar campos numéricos: input retorna string por padrão. Use Number() ou parseInt() no handler.

Misturar HTMLInputElement com HTMLSelectElement: cada elemento tem seu tipo. Handler genérico precisa de union type.

Não inferir tipo do Zod schema: criar interface manual quando z.infer já resolve, é retrabalho puro.

Ignorar tipagem do errors: errors.campo pode ser undefined, use optional chaining (errors.campo?.message).

Checklist: formulário tipado no React

Checklist Final

  • Interface criada pro estado do formulário
  • useState tipado com a interface
  • onChange tipado com ChangeEvent correto (Input, Select, Textarea)
  • onSubmit tipado com FormEvent
  • Campos numéricos convertidos no handler
  • react-hook-form configurado com generic da interface
  • Validação com Zod schema + z.infer pra gerar tipo
  • Sem nenhum 'any' nos eventos do formulário
  • errors tratados com optional chaining

Próximo passo: projeto completo com TypeScript

Formulários tipados são só uma peça. No CrazyStack, você constrói um projeto inteiro — API, front, validação, autenticação — tudo com TypeScript profissional do começo ao fim. Dá pra aplicar cada conceito que viu aqui em cenário real de mercado.