Aprenda / TypeScript do zero

IntermediárioAula 13 de 1416 min0% da trilha

Zod: tipo estático não é validação em runtime

Seu tipo Pedido promete um formato — mas nada confere se a API realmente devolveu isso, até o Zod entrar.

Lá na primeira lição desta trilha: os tipos do TypeScript são apagados na compilação — o navegador (ou o Node) nunca vê um único tipo, só JavaScript puro. Isso tem uma consequência que passa despercebida até morder: nenhum tipo protege seu código de dado errado vindo de fora.

O problema real: dado de fora não é confiável

interface Pedido {
  id: string;
  status: "pendente" | "pago" | "cancelado";
  total: number;
}

async function buscarPedido(id: string): Promise<Pedido> {
  const res = await fetch(`/api/pedidos/${id}`);
  const dado = await res.json();
  return dado as Pedido; // "as" é só uma afirmação — não checa NADA em runtime
}

const pedido = await buscarPedido("1");
pedido.total.toFixed(2);
// se a API mudou e "total" agora vem como string "99.90", isso compila liso
// e quebra em runtime: "pedido.total.toFixed is not a function"

O as Pedido não valida o formato do JSON — só diz pro compilador "trate isso como um Pedido, eu prometo que é". Se a promessa for falsa (API mudou, bug no backend, resposta de erro inesperada), o TS não tem como saber: ele já apagou toda essa checagem antes do código rodar.

Zod — validação de verdade, em runtime

Zod é uma biblioteca que descreve o formato esperado de um dado como um schema, e sabe checar, durante a execução, se um valor real bate com esse formato:

import { z } from "zod";

const PedidoSchema = z.object({
  id: z.string(),
  status: z.enum(["pendente", "pago", "cancelado"]),
  total: z.number(),
});

async function buscarPedido(id: string) {
  const res = await fetch(`/api/pedidos/${id}`);
  const bruto = await res.json();

  const resultado = PedidoSchema.safeParse(bruto);
  if (!resultado.success) {
    console.error("Formato inesperado:", resultado.error);
    return null;
  }

  return resultado.data; // aqui sim, checado de verdade — "total" É number, na prática
}

safeParse não lança exceção — devolve um objeto com success, exatamente como o discriminated union de resultado que você já usou nesta trilha. Se o dado real não bater com o schema (campo faltando, tipo errado), você descobre ali, com uma mensagem clara, em vez de um erro genérico três camadas depois.

z.infer — o tipo TypeScript nasce do schema, não o contrário

const PedidoSchema = z.object({
  id: z.string(),
  status: z.enum(["pendente", "pago", "cancelado"]),
  total: z.number(),
});

type Pedido = z.infer<typeof PedidoSchema>;
// { id: string; status: "pendente" | "pago" | "cancelado"; total: number }
// — o tipo é derivado do schema, então os dois nunca ficam dessincronizados

Sem z.infer, seria fácil manter uma interface Pedido e um PedidoSchema separados — e um dia mudar só um dos dois. Derivando o tipo do schema, existe uma única fonte de verdade.

Exercício

Escreva um schema Zod UsuarioSchema pra { nome: string; email: string; idade: number } e derive o tipo Usuario a partir dele com z.infer.

Quiz rápido

Por que const dado = await res.json() as Pedido não garante que dado realmente tem o formato de Pedido?
Qual é a vantagem de usar z.infer<typeof PedidoSchema> em vez de escrever a interface Pedido separadamente?