Voltar para o Blog

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 OK

Mas 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 incorreto

TypeScript 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:

  1. Define schemas que validam dados em runtime
  2. Infere tipos TypeScript automaticamente dos schemas
  3. 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)

📖 Ver Conteúdo Completo

Comentários (0)

Esse artigo ainda não possui comentários 😢. Seja o primeiro! 🚀🦅

Adicionar comentário