TypeScript em 2025: Como Validação Runtime com Zod Está Transformando Código Type-Safe em Produção
Olá HaWkers, você já se sentiu frustrado quando TypeScript compila perfeitamente mas sua aplicação quebra em produção por dados inesperados?
O problema é que TypeScript oferece type-safety APENAS em tempo de compilação. No runtime, JavaScript puro não tem ideia dos seus tipos, e dados vindos de APIs, formulários ou bancos de dados podem quebrar sua aplicação mesmo com TypeScript "100% tipado".
Em 2025, mais de 65% dos desenvolvedores usam TypeScript, e a comunidade descobriu que type-safety estático não é suficiente. A solução? Validação runtime com libraries como Zod, que trazem seus tipos TypeScript para o mundo real do JavaScript em execução.
O Problema: TypeScript Não Protege Runtime
Veja este cenário comum que quebra em produção:
// types.ts - Tipos perfeitos!
interface User {
id: number;
name: string;
email: string;
age: number;
}
// api.ts - Parece seguro com TypeScript
async function getUser(id: number): Promise<User> {
const response = await fetch(`/api/users/${id}`);
const user = await response.json() as User; // ⚠️ Mentira perigosa!
return user;
}
// app.ts - TypeScript está feliz
const user = await getUser(1);
console.log(user.name.toUpperCase()); // ✅ TypeScript OK
console.log(user.age + 10); // ✅ TypeScript OKMas em produção:
// API retorna isso (campo age como string!):
{
"id": 1,
"name": "João",
"email": "joao@example.com",
"age": "25"
}// Runtime: ERRO!
console.log(user.age + 10); // "2510" (string concatenation, não soma!)
// Pior: não quebra, mas comportamento incorretoTypeScript não pode prevenir isso porque as User é apenas uma "asserção de tipo" – você está dizendo ao compilador "confia em mim", mas no runtime, JavaScript não valida nada.
A Solução: Zod - Schema Validation + Type Inference
Zod é uma biblioteca de validação de schema que:
- Define schemas que validam dados em runtime
- Infere tipos TypeScript automaticamente dos schemas
- Garante type-safety tanto em compile-time quanto runtime
Exemplo: Reescrevendo com Zod
import { z } from 'zod';
// Schema define validação E tipos automaticamente
const UserSchema = z.object({
id: z.number().positive(),
name: z.string().min(1),
email: z.string().email(),
age: z.number().int().min(0).max(150)
});
// Tipo inferido automaticamente do schema!
type User = z.infer<typeof UserSchema>;
/*
type User = {
id: number;
name: string;
email: string;
age: number;
}
*/
// API com validação runtime
async function getUser(id: number): Promise<User> {
const response = await fetch(`/api/users/${id}`);
const data = await response.json();
// Valida em RUNTIME! Se falhar, lança erro
const user = UserSchema.parse(data);
return user; // Garantido type-safe
}
// Uso
try {
const user = await getUser(1);
console.log(user.age + 10); // ✅ Garantido que age é number!
} catch (error) {
if (error instanceof z.ZodError) {
console.error('Dados inválidos da API:', error.errors);
// [{ path: ['age'], message: 'Expected number, received string' }]
}
}Vantagens:
- ✅ Type-safety em compile-time E runtime
- ✅ Erros claros e específicos
- ✅ Tipos inferidos automaticamente (DRY - Don't Repeat Yourself)
- ✅ Validação de APIs, formulários, environment variables, etc.
Casos de Uso Avançados com Zod
1. Validação de Environment Variables
// env.ts - Valida variáveis de ambiente no startup
import { z } from 'zod';
const EnvSchema = z.object({
NODE_ENV: z.enum(['development', 'production', 'test']),
DATABASE_URL: z.string().url(),
API_KEY: z.string().min(32),
PORT: z.coerce.number().positive().default(3000), // Converte string → number
REDIS_HOST: z.string().default('localhost'),
MAX_RETRIES: z.coerce.number().int().min(1).max(10).default(3)
});
export type Env = z.infer<typeof EnvSchema>;
// Valida no startup da aplicação
export const env = EnvSchema.parse(process.env);
// Agora env é totalmente type-safe!
console.log(env.PORT + 1); // ✅ TypeScript sabe que é number
console.log(env.NODE_ENV); // ✅ Type: 'development' | 'production' | 'test'2. Validação de Formulários
import { z } from 'zod';
const RegisterFormSchema = z.object({
username: z.string()
.min(3, 'Usuário deve ter no mínimo 3 caracteres')
.max(20, 'Usuário deve ter no máximo 20 caracteres')
.regex(/^[a-zA-Z0-9_]+$/, 'Apenas letras, números e underscore'),
email: z.string()
.email('Email inválido')
.toLowerCase(), // Transforma em lowercase automaticamente
password: z.string()
.min(8, 'Senha deve ter no mínimo 8 caracteres')
.regex(/[A-Z]/, 'Senha deve conter ao menos uma letra maiúscula')
.regex(/[0-9]/, 'Senha deve conter ao menos um número'),
confirmPassword: z.string(),
age: z.coerce.number()
.int('Idade deve ser um número inteiro')
.min(18, 'Você deve ter pelo menos 18 anos')
.max(120, 'Idade inválida'),
termsAccepted: z.literal(true, {
errorMap: () => ({ message: 'Você deve aceitar os termos' })
})
}).refine(data => data.password === data.confirmPassword, {
message: 'Senhas não coincidem',
path: ['confirmPassword'] // Erro aparece neste campo
});
type RegisterForm = z.infer<typeof RegisterFormSchema>;
// Uso em React/Vue
function handleSubmit(formData: unknown) {
try {
const validData = RegisterFormSchema.parse(formData);
// validData é type-safe e validado!
await createUser(validData);
} catch (error) {
if (error instanceof z.ZodError) {
// Mostrar erros por campo
error.errors.forEach(err => {
console.log(`${err.path.join('.')}: ${err.message}`);
});
}
}
}3. Validação de Requisições API (tRPC-style)
import { z } from 'zod';
// Schemas para API endpoints
const CreatePostInput = z.object({
title: z.string().min(1).max(200),
content: z.string().min(10),
tags: z.array(z.string()).min(1).max(5),
published: z.boolean().default(false),
publishedAt: z.date().optional()
});
const UpdatePostInput = CreatePostInput.partial().extend({
id: z.number().positive()
});
// API Routes com validação automática
export const api = {
posts: {
create: async (input: unknown) => {
// Valida input
const data = CreatePostInput.parse(input);
// data é type-safe!
const post = await db.posts.create({
title: data.title,
content: data.content,
tags: data.tags,
published: data.published
});
return post;
},
update: async (input: unknown) => {
const data = UpdatePostInput.parse(input);
// TypeScript sabe que todos os campos são opcionais exceto id
const post = await db.posts.update({
where: { id: data.id },
data: {
...(data.title && { title: data.title }),
...(data.content && { content: data.content })
}
});
return post;
}
}
};
Zod vs. Alternativas
Comparação com Outras Bibliotecas
// Joi - Biblioteca mais antiga
import Joi from 'joi';
const joiSchema = Joi.object({
name: Joi.string().required(),
age: Joi.number().required()
});
// ❌ Problema: Tipos não são inferidos automaticamente
// Você precisa definir interface separada
interface User {
name: string;
age: number;
}
// Yup - Popular em React
import * as yup from 'yup';
const yupSchema = yup.object({
name: yup.string().required(),
age: yup.number().required()
});
// ⚠️ Tipos inferidos mas menos precisos que Zod
type User = yup.InferType<typeof yupSchema>;
// Zod - Melhor integração TypeScript
import { z } from 'zod';
const zodSchema = z.object({
name: z.string(),
age: z.number()
});
// ✅ Tipos inferidos perfeitamente
type User = z.infer<typeof zodSchema>;Por que Zod lidera em 2025:
- ✅ Zero dependencies
- ✅ Bundle size menor (~8kb minified)
- ✅ Type inference superior
- ✅ API mais intuitiva
- ✅ Melhor performance
- ✅ Comunidade ativa
Padrões Avançados com Zod
Transformações e Pré-processamento
// Transformar dados durante validação
const DateSchema = z.string()
.transform(str => new Date(str))
.refine(date => !isNaN(date.getTime()), 'Data inválida');
const UserWithDateSchema = z.object({
name: z.string(),
birthDate: DateSchema // String → Date automaticamente
});
const user = UserWithDateSchema.parse({
name: 'João',
birthDate: '1990-01-15'
});
console.log(user.birthDate); // Date object, não string!Schemas Condicionais
// Validação condicional baseada em outros campos
const PaymentSchema = z.discriminatedUnion('method', [
z.object({
method: z.literal('credit_card'),
cardNumber: z.string().length(16),
cvv: z.string().length(3),
expiryDate: z.string().regex(/^\d{2}\/\d{2}$/)
}),
z.object({
method: z.literal('pix'),
pixKey: z.string().email()
}),
z.object({
method: z.literal('bank_slip'),
// Sem campos adicionais necessários
})
]);
type Payment = z.infer<typeof PaymentSchema>;
// TypeScript entende cada variante!
function processPayment(payment: Payment) {
if (payment.method === 'credit_card') {
// TypeScript sabe que cardNumber existe aqui
console.log(payment.cardNumber);
} else if (payment.method === 'pix') {
// TypeScript sabe que pixKey existe aqui
console.log(payment.pixKey);
}
}Validação Assíncrona
const UniqueUsernameSchema = z.string()
.min(3)
.refine(async (username) => {
// Verifica no banco se username está disponível
const exists = await db.users.findUnique({
where: { username }
});
return !exists;
}, {
message: 'Username já está em uso'
});
// Uso
const result = await UniqueUsernameSchema.safeParseAsync('john_doe');
if (result.success) {
console.log('Username disponível:', result.data);
} else {
console.log('Erro:', result.error.errors);
}Integração com Frameworks Modernos
Next.js + tRPC + Zod
// server/routers/posts.ts
import { z } from 'zod';
import { router, publicProcedure } from '../trpc';
export const postsRouter = router({
create: publicProcedure
.input(z.object({
title: z.string().min(1),
content: z.string()
}))
.mutation(async ({ input }) => {
// input é automaticamente type-safe!
return db.posts.create({ data: input });
}),
list: publicProcedure
.input(z.object({
limit: z.number().min(1).max(100).default(10),
cursor: z.number().optional()
}))
.query(async ({ input }) => {
return db.posts.findMany({
take: input.limit,
...(input.cursor && { cursor: { id: input.cursor } })
});
})
});React Hook Form + Zod
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
const FormSchema = z.object({
email: z.string().email(),
password: z.string().min(8)
});
type FormData = z.infer<typeof FormSchema>;
function LoginForm() {
const {
register,
handleSubmit,
formState: { errors }
} = useForm<FormData>({
resolver: zodResolver(FormSchema) // Integração automática!
});
const onSubmit = (data: FormData) => {
// data é validado e type-safe
console.log(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('email')} />
{errors.email && <span>{errors.email.message}</span>}
<input type="password" {...register('password')} />
{errors.password && <span>{errors.password.message}</span>}
<button type="submit">Login</button>
</form>
);
}
Performance e Melhores Práticas
Reutilize Schemas
// ❌ Ruim: Recria schema em cada chamada
function validateUser(data: unknown) {
const schema = z.object({ name: z.string() });
return schema.parse(data);
}
// ✅ Bom: Define schema uma vez
const UserSchema = z.object({ name: z.string() });
function validateUser(data: unknown) {
return UserSchema.parse(data);
}Use .safeParse() para Controle de Erros
// ❌ parse() lança exceção
try {
const user = UserSchema.parse(data);
} catch (error) {
// Tratamento de erro
}
// ✅ safeParse() retorna resultado
const result = UserSchema.safeParse(data);
if (result.success) {
const user = result.data; // Type-safe
} else {
const errors = result.error.errors; // Erros estruturados
}Se você quer dominar TypeScript e padrões modernos de desenvolvimento, recomendo o artigo TypeScript em 2025: As Top 5 Práticas para Dominar JavaScript Tipado onde exploramos mais técnicas avançadas.
Bora pra cima! 🦅
💻 Domine TypeScript de Verdade
TypeScript + Zod representa o estado da arte em type-safety para 2025. Mas dominar TypeScript requer entender JavaScript profundamente primeiro.
Invista no Seu Futuro
Preparei um material completo para você dominar JavaScript e estar pronto para TypeScript avançado:
Formas de pagamento:
- R$9,90 (pagamento único)

