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
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 onSubmit={handleSubmit}>
<input name="name" onChange={handleChange} />
<input name="email" onChange={handleChange} />
<input name="age" type="number" onChange={handleChange} />
<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<HTMLInputElement>, 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 onSubmit = (data: LoginForm) => {
// data.email e data.password 100% tipados
console.log(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<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 onSubmit = (data: FormSchema) => {
// Validado em runtime E tipado em compile-time
console.log(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<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
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.