Validação de Formulário Type-Safe 2026
Valide formulários com zero chance de erro de tipagem. Regras tipadas, mensagens automáticas e inferência que faz o IDE trabalhar pra você.
Por que isso é importante
Validação de Formulário Type-Safe 2026. Valide formulários com zero chance de erro de tipagem. Regras tipadas, mensagens automáticas e inferência que faz o IDE trabalhar pra você.
Validação Sem Tipo: O Caos
Olha como validação sem tipo vira bagunça rapidinho. Sistema com strings soltas, nomes de campos que podem estar errados, regras que somem sem avisar:
// Validação sem tipo - problema esperando pra acontecer
const validateForm = (data: any) => {
const errors: any = {};
// Erro de digitação? Compilador não avisa
if (!data.emai) { // Deveria ser "email"
errors.email = "Email obrigatório";
}
// Campo novo adicionado? Esquece validar
// Campo "username" existe mas não tem validação
// Mudou nome do campo? Validação quebra silenciosamente
if (data.senha.length < 8) { // Campo mudou pra "password"
errors.senha = "Senha muito curta";
}
return errors;
};
// Uso: erro só aparece em runtime
const formData = {
email: "teste@email.com",
username: "joao",
password: "123" // Validação não pega esse campo
};
const errors = validateForm(formData);
console.log(errors.email); // undefined - validou campo errado
console.log(errors.password); // undefined - não tem validaçãoEsse código compila tranquilo. Roda sem erro. Só que está cheio de bugs que vão aparecer quando usuário tentar usar o formulário. Galera vai conseguir cadastrar com senha fraca, campos obrigatórios vão passar batido, mensagens de erro vão aparecer no lugar errado.
Problema maior? Quando você refatora. Mudou nome de campo, agora precisa caçar todas as validações que usam aquele campo. Esqueceu uma? Bug em produção. Adicionou campo novo? Precisa lembrar de adicionar validação. Esqueceu? Bug em produção.
Tipando Regras de Validação
Primeiro passo: criar uma estrutura tipada para regras de validação. Cada regra precisa de uma função de validação e uma mensagem de erro. Genéricos garantem que a regra só aceita valores do tipo certo:
// Regra de validação tipada
type ValidationRule<T> = {
validate: (value: T) => boolean;
message: string;
};
// Regra para strings - só aceita string
const required: ValidationRule<string> = {
validate: (value) => value.trim().length > 0,
message: "Campo obrigatório"
};
// Regra para números - só aceita number
const minAge: ValidationRule<number> = {
validate: (value) => value >= 18,
message: "Idade mínima: 18 anos"
};
// Regra customizada com parâmetro
const minLength = (min: number): ValidationRule<string> => ({
validate: (value) => value.length >= min,
message: `Mínimo ${min} caracteres`
});
const maxLength = (max: number): ValidationRule<string> => ({
validate: (value) => value.length <= max,
message: `Máximo ${max} caracteres`
});Agora cada regra é tipada. Tenta passar número pra regra de string? Erro de compilação. Tenta validar campo que não existe? Erro de compilação. IDE completa automaticamente os campos que podem ser validados.
Dá pra compor regras. Quer validar que campo é obrigatório E tem no mínimo 8 caracteres? Cria array de regras:
// Múltiplas regras para um campo
type FieldRules<T> = ValidationRule<T>[];
// Senha: obrigatória + mínimo 8 chars + máximo 32 chars
const passwordRules: FieldRules<string> = [
required,
minLength(8),
maxLength(32)
];
// Email: obrigatório + formato válido
const emailPattern: ValidationRule<string> = {
validate: (value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value),
message: "Email inválido"
};
const emailRules: FieldRules<string> = [
required,
emailPattern
];FormValidator Genérico
Agora vem a mágica: validador genérico que infere tipos do formulário. Sistema que garante que cada campo só pode ter regras compatíveis com seu tipo:
// Tipo do formulário
interface UserForm {
email: string;
password: string;
age: number;
terms: boolean;
}
// Validação para um campo específico
type FieldValidation<T, K extends keyof T> = {
field: K;
rules: ValidationRule<T[K]>[];
};
// Erros de validação - espelha estrutura do form
type ValidationErrors<T> = {
[K in keyof T]?: string[];
};
// Resultado da validação
type ValidationResult<T> = {
isValid: boolean;
errors: ValidationErrors<T>;
};
// Validador genérico
class FormValidator<T> {
private validations: FieldValidation<T, keyof T>[] = [];
// Adiciona validação para um campo
field<K extends keyof T>(
field: K,
rules: ValidationRule<T[K]>[]
): this {
this.validations.push({ field, rules });
return this;
}
// Valida todos os campos
validate(data: T): ValidationResult<T> {
const errors: ValidationErrors<T> = {};
for (const validation of this.validations) {
const value = data[validation.field];
const fieldErrors: string[] = [];
for (const rule of validation.rules) {
if (!rule.validate(value)) {
fieldErrors.push(rule.message);
}
}
if (fieldErrors.length > 0) {
errors[validation.field] = fieldErrors;
}
}
return {
isValid: Object.keys(errors).length === 0,
errors
};
}
}Implementação toda tipada. Tenta adicionar regra de string em campo number? Erro de compilação. Tenta validar campo que não existe no formulário? Erro de compilação. Acessa erro de campo que não existe? Erro de compilação.
Simples assim. Mas o poder está na inferência: TypeScript sabe exatamente quais campos podem ter quais regras, quais erros podem aparecer, e garante que tudo bate.
Inferência de Tipos no Resultado
Melhor parte do sistema tipado: objeto de erros espelha exatamente a estrutura do formulário. TypeScript sabe quais campos podem ter erros e te ajuda a acessar eles corretamente:
// Configura validador
const validator = new FormValidator<UserForm>()
.field("email", [required, emailPattern])
.field("password", [required, minLength(8), maxLength(32)])
.field("age", [minAge]);
// Valida dados
const formData: UserForm = {
email: "",
password: "123",
age: 15,
terms: false
};
const result = validator.validate(formData);
// TypeScript sabe exatamente o tipo de result.errors
if (!result.isValid) {
// ✅ Acesso válido - campo existe
console.log(result.errors.email); // string[] | undefined
// ✅ Acesso válido - campo existe
console.log(result.errors.password);
// ❌ Erro de compilação - campo não existe
// console.log(result.errors.emaill); // Typo detectado!
// ❌ Erro de compilação - campo não validado
// console.log(result.errors.terms); // Sem validação = sem erro possível
}
// IDE completa automaticamente campos disponíveis
result.errors. // email | password | ageOlha a mágica: adicionou validação em campo novo? Objeto de erros automaticamente inclui esse campo. Removeu validação de um campo? TypeScript para de permitir acesso aos erros daquele campo. Mudou nome de campo? Todos os lugares que acessam erros mostram erro de compilação até você atualizar.
Dá pra usar isso pra renderizar erros dinamicamente, sempre com segurança de tipos:
// Componente React tipado
function FormErrors<T>({
errors,
field
}: {
errors: ValidationErrors<T>;
field: keyof T;
}) {
const fieldErrors = errors[field];
if (!fieldErrors || fieldErrors.length === 0) {
return null;
}
return (
<div className="errors">
{fieldErrors.map((error, index) => (
<p key={index} className="error">{error}</p>
))}
</div>
);
}
// Uso - field precisa ser chave válida do formulário
<FormErrors errors={result.errors} field="email" /> // ✅
<FormErrors errors={result.errors} field="emaill" /> // ❌ Erro de compilaçãoRegras Compostas e Reutilizáveis
Biblioteca de regras prontas que você usa em qualquer formulário. Cada regra totalmente tipada, reutilizável, testável:
// validation-rules.ts - biblioteca de regras
// String rules
export const required: ValidationRule<string> = {
validate: (value) => value.trim().length > 0,
message: "Campo obrigatório"
};
export const minLength = (min: number): ValidationRule<string> => ({
validate: (value) => value.length >= min,
message: `Mínimo ${min} caracteres`
});
export const maxLength = (max: number): ValidationRule<string> => ({
validate: (value) => value.length <= max,
message: `Máximo ${max} caracteres`
});
export const email: ValidationRule<string> = {
validate: (value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value),
message: "Email inválido"
};
export const pattern = (
regex: RegExp,
message: string
): ValidationRule<string> => ({
validate: (value) => regex.test(value),
message
});
// Number rules
export const min = (minimum: number): ValidationRule<number> => ({
validate: (value) => value >= minimum,
message: `Valor mínimo: ${minimum}`
});
export const max = (maximum: number): ValidationRule<number> => ({
validate: (value) => value <= maximum,
message: `Valor máximo: ${maximum}`
});
export const range = (
minimum: number,
maximum: number
): ValidationRule<number> => ({
validate: (value) => value >= minimum && value <= maximum,
message: `Valor entre ${minimum} e ${maximum}`
});
// Boolean rules
export const mustBeTrue: ValidationRule<boolean> = {
validate: (value) => value === true,
message: "Campo precisa ser aceito"
};Agora você tem biblioteca pronta pra usar em qualquer formulário. Quer validar CPF? Adiciona regra. Quer validar telefone? Adiciona regra. Quer validar URL? Adiciona regra. Tudo tipado, tudo reutilizável:
// Regras customizadas para domínio específico
export const cpf: ValidationRule<string> = {
validate: (value) => {
const cleaned = value.replace(/\D/g, '');
if (cleaned.length !== 11) return false;
// Lógica de validação de CPF
return true;
},
message: "CPF inválido"
};
export const phone: ValidationRule<string> = {
validate: (value) => /^\([0-9]{2}\) [0-9]{4,5}-[0-9]{4}$/.test(value),
message: "Telefone inválido. Formato: (99) 99999-9999"
};
export const url: ValidationRule<string> = {
validate: (value) => {
try {
new URL(value);
return true;
} catch {
return false;
}
},
message: "URL inválida"
};
// Usa todas as regras
interface ContactForm {
name: string;
email: string;
phone: string;
website: string;
document: string;
age: number;
terms: boolean;
}
const contactValidator = new FormValidator<ContactForm>()
.field("name", [required, minLength(3), maxLength(100)])
.field("email", [required, email])
.field("phone", [required, phone])
.field("website", [url])
.field("document", [required, cpf])
.field("age", [min(18), max(120)])
.field("terms", [mustBeTrue]);Regras compostas garantem que validação é consistente em todo o app. Mudou regra de email? Muda em um lugar, afeta todos os formulários. Quer testar validação de CPF? Testa a regra isoladamente, não precisa testar em cada formulário que usa.
Integrando com React Hook Form
React Hook Form é popular mas não vem com validação tipada por padrão. Dá pra conectar nosso validador tipado com ele e ter o melhor dos dois mundos: performance do Hook Form e segurança de tipos do nosso sistema:
import { useForm } from 'react-hook-form';
interface LoginForm {
email: string;
password: string;
}
// Validador tipado
const loginValidator = new FormValidator<LoginForm>()
.field("email", [required, email])
.field("password", [required, minLength(8)]);
// Hook personalizado que conecta os dois
function useTypedForm<T>(validator: FormValidator<T>) {
const form = useForm<T>();
const validateForm = (data: T) => {
const result = validator.validate(data);
if (!result.isValid) {
// Converte erros pro formato do React Hook Form
Object.entries(result.errors).forEach(([field, messages]) => {
form.setError(
field as keyof T,
{
type: 'manual',
message: (messages as string[])[0]
}
);
});
return false;
}
return true;
};
return { ...form, validateForm };
}
// Componente
function LoginPage() {
const { register, handleSubmit, validateForm, formState } =
useTypedForm(loginValidator);
const LoginForm) => {
if (validateForm(data)) {
console.log('Form válido:', data);
}
};
return (
<form
<input {...register('email')} />
{formState.errors.email && (
<span>{formState.errors.email.message}</span>
)}
<input type="password" {...register('password')} />
{formState.errors.password && (
<span>{formState.errors.password.message}</span>
)}
<button type="submit">Entrar</button>
</form>
);
}Agora você tem performance do React Hook Form com segurança de tipos do nosso validador. IDE completa campos automaticamente, compilador garante que validações batem com campos, refatoração é segura.
Dá pra ir além e validar em tempo real conforme usuário digita:
// Validação em tempo real por campo
function useFieldValidator<T, K extends keyof T>(
field: K,
rules: ValidationRule<T[K]>[]
) {
const [errors, setErrors] = React.useState<string[]>([]);
const validate = (value: T[K]) => {
const fieldErrors: string[] = [];
for (const rule of rules) {
if (!rule.validate(value)) {
fieldErrors.push(rule.message);
}
}
setErrors(fieldErrors);
return fieldErrors.length === 0;
};
return { errors, validate };
}
// Uso
function EmailInput() {
const [value, setValue] = React.useState('');
const { errors, validate } = useFieldValidator<LoginForm, 'email'>(
'email',
[required, email]
);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newValue = e.target.value;
setValue(newValue);
validate(newValue);
};
return (
<div>
<input
value={value}
className={errors.length > 0 ? 'invalid' : ''}
/>
{errors.map((error, i) => (
<span key={i} className="error">{error}</span>
))}
</div>
);
}Validação Assíncrona Tipada
Algumas validações precisam checar com servidor: email já cadastrado, username disponível, CEP válido. Dá pra tipar validação assíncrona também:
// Regra assíncrona
type AsyncValidationRule<T> = {
validate: (value: T) => Promise<boolean>;
message: string;
};
// Validação de campo com regras assíncronas
type FieldValidationAsync<T, K extends keyof T> = {
field: K;
rules: ValidationRule<T[K]>[];
asyncRules?: AsyncValidationRule<T[K]>[];
};
// Resultado assíncrono
type AsyncValidationResult<T> = Promise<ValidationResult<T>>;
// Validador com suporte assíncrono
class AsyncFormValidator<T> {
private validations: FieldValidationAsync<T, keyof T>[] = [];
field<K extends keyof T>(
field: K,
rules: ValidationRule<T[K]>[],
asyncRules?: AsyncValidationRule<T[K]>[]
): this {
this.validations.push({ field, rules, asyncRules });
return this;
}
async validate(data: T): AsyncValidationResult<T> {
const errors: ValidationErrors<T> = {};
for (const validation of this.validations) {
const value = data[validation.field];
const fieldErrors: string[] = [];
// Validações síncronas primeiro
for (const rule of validation.rules) {
if (!rule.validate(value)) {
fieldErrors.push(rule.message);
}
}
// Se passou validações síncronas, roda assíncronas
if (fieldErrors.length === 0 && validation.asyncRules) {
for (const rule of validation.asyncRules) {
const isValid = await rule.validate(value);
if (!isValid) {
fieldErrors.push(rule.message);
}
}
}
if (fieldErrors.length > 0) {
errors[validation.field] = fieldErrors;
}
}
return {
isValid: Object.keys(errors).length === 0,
errors
};
}
}Regras assíncronas comuns em formulários de cadastro:
// Regras assíncronas prontas
const uniqueEmail: AsyncValidationRule<string> = {
validate: async (value) => {
const response = await fetch(`/api/check-email?email=${value}`);
const data = await response.json();
return data.available;
},
message: "Email já cadastrado"
};
const availableUsername: AsyncValidationRule<string> = {
validate: async (value) => {
const response = await fetch(`/api/check-username?username=${value}`);
const data = await response.json();
return data.available;
},
message: "Username indisponível"
};
const validCEP: AsyncValidationRule<string> = {
validate: async (value) => {
const cleaned = value.replace(/\D/g, '');
if (cleaned.length !== 8) return false;
const response = await fetch(`https://viacep.com.br/ws/${cleaned}/json/`);
const data = await response.json();
return !data.erro;
},
message: "CEP não encontrado"
};
// Formulário de registro com validações assíncronas
interface RegisterForm {
username: string;
email: string;
password: string;
cep: string;
}
const registerValidator = new AsyncFormValidator<RegisterForm>()
.field(
"username",
[required, minLength(3), maxLength(20)],
[availableUsername] // Valida disponibilidade depois
)
.field(
"email",
[required, email],
[uniqueEmail] // Valida unicidade depois
)
.field(
"password",
[required, minLength(8), maxLength(32)]
)
.field(
"cep",
[required, pattern(/^[0-9]{5}-[0-9]{3}$/, "Formato: 00000-000")],
[validCEP] // Valida com API depois
);
// Uso
async function handleSubmit(data: RegisterForm) {
const result = await registerValidator.validate(data);
if (!result.isValid) {
console.log('Erros:', result.errors);
return;
}
console.log('Formulário válido:', data);
}Sistema valida síncrono primeiro: formato, tamanho, obrigatoriedade. Se tudo passar, aí sim faz chamadas pra API. Evita request desnecessário quando campo já está visivelmente inválido.
Comparação com Zod e Yup
Sistema Type-Safe Custom
+ Prós
- • Controle total sobre validação
- • Zero dependências externas
- • Regras customizadas fáceis
- • Integração simples com qualquer framework
- • Bundle pequeno (só o que você usa)
- • Aprende TypeScript genéricos na prática
− Contras
- • Precisa implementar regras do zero
- • Sem ecosystem de plugins prontos
- • Validação assíncrona requer mais código
- • Menos recursos out-of-the-box
Zod
+ Prós
- • Inferência de tipos automática
- • API declarativa poderosa
- • Transformações de dados built-in
- • Validação assíncrona nativa
- • Mensagens de erro customizáveis
- • Ecosystem rico de integrações
− Contras
- • Bundle maior (~14kb minified)
- • Curva de aprendizado da API
- • Pode ser overkill para forms simples
- • Performance levemente menor em forms grandes
Yup
+ Prós
- • API madura e testada
- • Integração nativa com Formik
- • Validação assíncrona robusta
- • Schema reusáveis fáceis
- • Mensagens de erro i18n
− Contras
- • Tipos menos precisos que Zod
- • API baseada em métodos encadeados
- • Bundle maior que solução custom
- • Menos type-safe que alternativas
Checklist de Validação Type-Safe
- Definir interface do formulário com tipos precisos
- Criar ValidationRule
genérico para cada tipo de campo - Implementar biblioteca de regras reutilizáveis (email, required, min, max)
- Criar FormValidator
genérico que infere tipos - Garantir que ValidationErrors
espelha estrutura do form - Implementar validação síncrona para regras básicas
- Adicionar AsyncValidationRule
para validações com API - Integrar com React Hook Form ou framework de escolha
- Criar hook useTypedForm para conectar validador com form library
- Implementar validação em tempo real por campo com useFieldValidator
- Testar que IDE completa campos automaticamente
- Verificar que compilador detecta typos em nomes de campos
- Confirmar que refatoração de campos atualiza validações automaticamente
- Adicionar mensagens de erro descritivas em português
- Implementar feedback visual de erros no UI
- Otimizar performance com debounce em validações assíncronas