TypeScript

Zod em TypeScript: Validação de Schemas e Inferência de Tipos na Prática

12 min de leitura

Zod em TypeScript: Validação de Schemas e Inferência de Tipos na Prática

O que é Zod e por que você precisa Zod é uma biblioteca TypeScript-first para validação de schemas e inferência de tipos. Ela resolve um problema fundamental no desenvolvimento moderno: garantir que dados em tempo de execução correspondam aos tipos que você declarou em TypeScript. Enquanto TypeScript oferece segurança de tipo em tempo de compilação, os dados que chegam da API, formulários ou banco de dados precisam ser validados em tempo de execução. Zod preenche essa lacuna de forma elegante e performática. A principal vantagem do Zod sobre alternativas como Joi ou Yup é sua abordagem orientada a tipos. Você define um schema uma única vez e Zod automaticamente infere o tipo TypeScript correspondente. Não há duplicação de código, não há schemas separados de tipos — tudo vem de um único source of truth. Isso reduz bugs e torna o código mais mantível em grandes aplicações. Conceitos Fundamentais Entendendo Schemas Um schema no Zod é uma descrição estruturada de como

<h2>O que é Zod e por que você precisa</h2>

<p>Zod é uma biblioteca TypeScript-first para validação de schemas e inferência de tipos. Ela resolve um problema fundamental no desenvolvimento moderno: garantir que dados em tempo de execução correspondam aos tipos que você declarou em TypeScript. Enquanto TypeScript oferece segurança de tipo em tempo de compilação, os dados que chegam da API, formulários ou banco de dados precisam ser validados em tempo de execução. Zod preenche essa lacuna de forma elegante e performática.</p>

<p>A principal vantagem do Zod sobre alternativas como Joi ou Yup é sua abordagem orientada a tipos. Você define um schema uma única vez e Zod automaticamente infere o tipo TypeScript correspondente. Não há duplicação de código, não há schemas separados de tipos — tudo vem de um único source of truth. Isso reduz bugs e torna o código mais mantível em grandes aplicações.</p>

<h2>Conceitos Fundamentais</h2>

<h3>Entendendo Schemas</h3>

<p>Um schema no Zod é uma descrição estruturada de como os dados devem se parecer. Você começa com tipos primitivos como <code>z.string()</code>, <code>z.number()</code>, <code>z.boolean()</code> e constrói estruturas mais complexas a partir deles. Cada schema é uma instância de uma classe que sabe validar dados e produzir mensagens de erro detalhadas.</p>

<p>Quando você chama <code>.parse()</code> em um schema, Zod valida os dados. Se falharem, lança uma exceção com informações sobre o que deu errado. Existe também <code>.safeParse()</code> que retorna um objeto com a propriedade <code>success</code> e os dados ou erros, sem lançar exceção. Essa flexibilidade permite que você escolha como lidar com validações falhadas.</p>

<pre><code class="language-typescript">import { z } from &#039;zod&#039;;

// Schema simples

const usuarioSchema = z.object({

nome: z.string().min(3, &#039;Nome deve ter no mínimo 3 caracteres&#039;),

email: z.string().email(&#039;Email inválido&#039;),

idade: z.number().int().positive(&#039;Idade deve ser um número positivo&#039;),

ativo: z.boolean().default(true),

});

// Validação com parse (lança erro)

const usuario = usuarioSchema.parse({

nome: &#039;João Silva&#039;,

email: &#039;joao@example.com&#039;,

idade: 28,

});

// Validação com safeParse (retorna resultado)

const resultado = usuarioSchema.safeParse({

nome: &#039;Jo&#039;,

email: &#039;email-invalido&#039;,

idade: -5,

});

if (!resultado.success) {

console.log(resultado.error.errors);

// [

// { message: &#039;Nome deve ter no mínimo 3 caracteres&#039;, ... },

// { message: &#039;Email inválido&#039;, ... },

// { message: &#039;Idade deve ser um número positivo&#039;, ... }

// ]

}</code></pre>

<h3>Inferência de Tipos</h3>

<p>A inferência de tipos é onde Zod brilha. Ao invés de escrever uma interface TypeScript manualmente e depois um schema Zod que replique essa interface, você escreve apenas o schema e deixa Zod inferir o tipo automaticamente usando <code>z.infer&lt;typeof&gt;</code>. Isso garante que seu tipo sempre corresponde exatamente ao seu schema.</p>

<p>Quando você precisa usar o tipo em anotações TypeScript, basta usar <code>z.infer</code>. Isso cria uma relação bidirecional automática: mude o schema, e o tipo muda junto. Não há chance de sincronização manual falhar.</p>

<pre><code class="language-typescript">import { z } from &#039;zod&#039;;

const usuarioSchema = z.object({

id: z.number(),

nome: z.string(),

email: z.string().email(),

telefone: z.string().optional(),

permissoes: z.array(z.enum([&#039;leitura&#039;, &#039;escrita&#039;, &#039;admin&#039;])),

});

// Infer o tipo automaticamente

type Usuario = z.infer&lt;typeof usuarioSchema&gt;;

// Agora Usuario é:

// {

// id: number;

// nome: string;

// email: string;

// telefone?: string | undefined; // permissoes: (&#039;leitura&#039; | &#039;escrita&#039; | &#039;admin&#039;)[];

// }

function processarUsuario(usuario: Usuario) {

console.log(Processando ${usuario.nome} com email ${usuario.email});

}

const dados = usuarioSchema.parse({

id: 1,

nome: &#039;Maria&#039;,

email: &#039;maria@example.com&#039;,

permissoes: [&#039;leitura&#039;, &#039;escrita&#039;],

});

processarUsuario(dados); // TypeScript sabe que dados é Usuario</code></pre>

<h2>Técnicas Avançadas de Validação</h2>

<h3>Refinamentos Customizados</h3>

<p>Nem toda validação pode ser expressa com os validadores built-in do Zod. Para lógica complexa ou validações entre campos, você usa <code>.refine()</code> ou <code>.superRefine()</code>. O método <code>refine</code> recebe uma função que retorna um booleano, enquanto <code>superRefine</code> oferece mais controle sobre as mensagens de erro.</p>

<p>Refinamentos são poderosos para validações que envolvem múltiplos campos ou lógica de negócio. Por exemplo, validar se a data de fim é posterior à data de início, ou se uma senha contém caracteres especiais. Eles são executados após as validações primitivas passarem.</p>

<pre><code class="language-typescript">import { z } from &#039;zod&#039;;

const agendamentoSchema = z

.object({

dataInicio: z.coerce.date(),

dataFim: z.coerce.date(),

descricao: z.string().min(5),

})

.refine((data) =&gt; data.dataFim &gt; data.dataInicio, {

message: &#039;Data de fim deve ser posterior à data de início&#039;,

path: [&#039;dataFim&#039;], // Indica qual campo tem o erro

});

const senhaSchema = z

.string()

.min(8, &#039;Senha deve ter no mínimo 8 caracteres&#039;)

.superRefine((val, ctx) =&gt; {

if (!/[A-Z]/.test(val)) {

ctx.addIssue({

code: z.ZodIssueCode.custom,

message: &#039;Deve conter pelo menos uma letra maiúscula&#039;,

});

}

if (!/[0-9]/.test(val)) {

ctx.addIssue({

code: z.ZodIssueCode.custom,

message: &#039;Deve conter pelo menos um número&#039;,

});

}

if (!/[!@#$%^&amp;*]/.test(val)) {

ctx.addIssue({

code: z.ZodIssueCode.custom,

message: &#039;Deve conter pelo menos um caractere especial&#039;,

});

}

});

const agendamento = agendamentoSchema.parse({

dataInicio: &#039;2024-01-15&#039;,

dataFim: &#039;2024-01-16&#039;,

descricao: &#039;Reunião importante&#039;,

});

const resultado = senhaSchema.safeParse(&#039;abc123&#039;);

// Vai falhar com todas as três mensagens customizadas</code></pre>

<h3>Transformações</h3>

<p>Transformações permitem modificar o valor após validação bem-sucedida. Use <code>.transform()</code> para normalizar dados: converter strings para maiúsculas, parsear JSON, processar datas, remover espaços em branco. Transformações são aplicadas apenas após todas as validações passarem e garantem que o dado retornado é consistente com o que você espera.</p>

<pre><code class="language-typescript">import { z } from &#039;zod&#039;;

const usuarioRegistroSchema = z.object({

nome: z.string().transform((val) =&gt; val.trim().toLowerCase()),

email: z

.string()

.email()

.transform((val) =&gt; val.toLowerCase()),

dataNascimento: z.coerce.date().transform((date) =&gt; {

const hoje = new Date();

return Math.floor((hoje.getTime() - date.getTime()) / (365.25 24 60 60 1000));

}),

preferencias: z

.string()

.optional()

.transform((val) =&gt; {

if (!val) return {};

try {

return JSON.parse(val);

} catch {

return {};

}

}),

});

const resultado = usuarioRegistroSchema.parse({

nome: &#039; João Silva &#039;,

email: &#039;JOAO@EXAMPLE.COM&#039;,

dataNascimento: &#039;1995-05-20&#039;,

preferencias: &#039;{&quot;tema&quot;:&quot;escuro&quot;,&quot;idioma&quot;:&quot;pt&quot;}&#039;,

});

console.log(resultado);

// {

// nome: &#039;joão silva&#039;,

// email: &#039;joao@example.com&#039;,

// dataNascimento: 28,

// preferencias: { tema: &#039;escuro&#039;, idioma: &#039;pt&#039; }

// }</code></pre>

<h2>Casos de Uso Práticos</h2>

<h3>Validação de APIs e Formulários</h3>

<p>Em uma API real, você precisa validar dados que chegam de clientes. Zod se integra perfeitamente com frameworks como Express, Fastify e Next.js. Defina um schema, valide o request body e tenha segurança de tipo garantida em todo o handler.</p>

<pre><code class="language-typescript">import { z } from &#039;zod&#039;;

import express from &#039;express&#039;;

const app = express();

app.use(express.json());

// Schema para criar um post

const criarPostSchema = z.object({

titulo: z.string().min(5).max(200),

conteudo: z.string().min(20).max(10000),

tags: z.array(z.string()).max(5),

publicado: z.boolean().default(false),

});

type CriarPost = z.infer&lt;typeof criarPostSchema&gt;;

app.post(&#039;/posts&#039;, (req, res) =&gt; {

const resultado = criarPostSchema.safeParse(req.body);

if (!resultado.success) {

return res.status(400).json({

erros: resultado.error.flatten(),

});

}

const post: CriarPost = resultado.data;

// Aqui você tem certeza absoluta que post tem a estrutura correta

console.log(Post criado: ${post.titulo});

res.json({ id: 1, ...post });

});</code></pre>

<h3>Configuração de Ambientes</h3>

<p>Aplicações precisam carregar e validar variáveis de ambiente. Zod garante que você tem todas as variáveis necessárias e com os tipos corretos antes que a aplicação inicie.</p>

<pre><code class="language-typescript">import { z } from &#039;zod&#039;;

const envSchema = z.object({

NODE_ENV: z.enum([&#039;development&#039;, &#039;production&#039;, &#039;test&#039;]),

PORT: z.coerce.number().int().positive().default(3000),

DATABASE_URL: z.string().url(),

JWT_SECRET: z.string().min(32),

API_TIMEOUT: z.coerce.number().int().positive().default(30000),

LOG_LEVEL: z

.enum([&#039;error&#039;, &#039;warn&#039;, &#039;info&#039;, &#039;debug&#039;])

.default(&#039;info&#039;),

});

// No início da sua aplicação

const env = envSchema.parse(process.env);

// Agora env é tipado e você sabe que tem todos os valores necessários

export const config = {

nodeEnv: env.NODE_ENV,

port: env.PORT,

databaseUrl: env.DATABASE_URL,

jwtSecret: env.JWT_SECRET,

apiTimeout: env.API_TIMEOUT,

logLevel: env.LOG_LEVEL,

};</code></pre>

<h3>Composição de Schemas</h3>

<p>Em projetos grandes, você raramente escreve um schema único gigante. Zod permite compor schemas menores em estruturas maiores. Use <code>.merge()</code>, <code>.extend()</code> ou neste em objetos maiores. Isso mantém o código DRY e reutilizável.</p>

<pre><code class="language-typescript">import { z } from &#039;zod&#039;;

// Schemas base reutilizáveis

const baseSchema = z.object({

id: z.number().int().positive(),

criadoEm: z.coerce.date().default(() =&gt; new Date()),

atualizadoEm: z.coerce.date(),

});

const usuarioSchema = baseSchema.extend({

nome: z.string().min(3),

email: z.string().email(),

ativo: z.boolean().default(true),

});

const postSchema = baseSchema.extend({

titulo: z.string().min(5),

conteudo: z.string(),

autorId: z.number().int().positive(),

publicado: z.boolean().default(false),

});

// Composição: um usuário com seus posts

const usuarioComPostsSchema = usuarioSchema.extend({

posts: z.array(postSchema),

});

type Usuario = z.infer&lt;typeof usuarioSchema&gt;;

type Post = z.infer&lt;typeof postSchema&gt;;

type UsuarioComPosts = z.infer&lt;typeof usuarioComPostsSchema&gt;;

// Dados complexos agora são tipados e validados perfeitamente

const dados = usuarioComPostsSchema.parse({

id: 1,

nome: &#039;Alice&#039;,

email: &#039;alice@example.com&#039;,

criadoEm: &#039;2024-01-01&#039;,

atualizadoEm: &#039;2024-01-15&#039;,

posts: [

{

id: 100,

titulo: &#039;Meu Primeiro Post&#039;,

conteudo: &#039;Conteúdo interessante aqui...&#039;,

autorId: 1,

criadoEm: &#039;2024-01-10&#039;,

atualizadoEm: &#039;2024-01-15&#039;,

},

],

});</code></pre>

<h2>Conclusão</h2>

<p>Zod transforma a forma como você lida com validação em TypeScript. Três pontos principais consolidam esse aprendizado:</p>

<p><strong>Primeiro</strong>, a inferência automática de tipos elimina a duplicação código que plaga projetos que usam validadores tradicionais. Seu schema é fonte única da verdade, reduzindo erros e facilitando manutenção em equipes.</p>

<p><strong>Segundo</strong>, a composição e refinamento de schemas permitem expressar lógica de validação complexa de forma limpa e testável. Não há necessidade de dispersar validação em múltiplos lugares da aplicação.</p>

<p><strong>Terceiro</strong>, a integração com frameworks e padrões modernos (APIs, formulários, variáveis de ambiente) torna Zod uma ferramenta prática do dia a dia, não um overhead teórico. Qualidade de tipo em tempo de execução se torna tão natural quanto type checking estático.</p>

<h2>Referências</h2>

<ul>

<li><a href="https://zod.dev" target="_blank" rel="noopener noreferrer">Documentação Oficial do Zod</a></li>

<li><a href="https://www.typescriptlang.org/docs/handbook/utility-types.html" target="_blank" rel="noopener noreferrer">Zod no TypeScript Deep Dive - Type Inference</a></li>

<li><a href="https://dev.to/franciscomendes10866/api-validation-in-express-with-zod-6d8" target="_blank" rel="noopener noreferrer">API Validation in Express with Zod - Dev.to</a></li>

<li><a href="https://blog.logrocket.com/using-zod-for-runtime-type-validation-in-typescript/" target="_blank" rel="noopener noreferrer">Runtime Type Validation Patterns - LogRocket</a></li>

<li><a href="https://github.com/colinhacks/zod" target="_blank" rel="noopener noreferrer">Zod GitHub Repository</a></li>

</ul>

<p>&lt;!-- FIM --&gt;</p>

Comentários

Mais em TypeScript

O que Todo Dev Deve Saber sobre Inversão de Dependência com TypeScript: tsyringe e InversifyJS
O que Todo Dev Deve Saber sobre Inversão de Dependência com TypeScript: tsyringe e InversifyJS

Compreendendo Inversão de Dependência A inversão de dependência é um princípi...

Boas Práticas de Escrevendo Arquivos .d.ts: Tipando Bibliotecas JavaScript Existentes para Times Ágeis
Boas Práticas de Escrevendo Arquivos .d.ts: Tipando Bibliotecas JavaScript Existentes para Times Ágeis

O que são Arquivos .d.ts e Por Que Importam Os arquivos (TypeScript Declarati...

Componentes Genéricos em React com TypeScript: Do Básico ao Avançado
Componentes Genéricos em React com TypeScript: Do Básico ao Avançado

O que são Componentes Genéricos em React com TypeScript? Componentes genérico...