TypeScript de Cero a Experto / TS en el mundo real
Zod: validar en runtime, tipar en compilación
Los tipos se borran, así que no protegen del mundo exterior. Escribir type guards a mano para cada dato es tedioso y frágil. Zod resuelve ambos problemas de un golpe: defines un ESQUEMA una vez, y obtienes DOS cosas sincronizadas —la validación en runtime (que sí corre) y el tipo de TypeScript (que se infiere del esquema). Es el puente definitivo entre los datos externos y tus tipos, y el estándar de facto del ecosistema.
El inspector de aduana con un manual
Instalar y definir un esquema
Zod se instala como cualquier dependencia (pnpm add zod) y se usa definiendo
esquemas que describen la forma de tus datos:
import { z } from "zod";
// un esquema: describe la forma Y las reglas de validación
const UsuarioSchema = z.object({
id: z.number(),
nombre: z.string().min(1), // string no vacío
email: z.string().email(), // formato de email válido
edad: z.number().min(0).optional(), // opcional, no negativo
rol: z.enum(["admin", "editor", "lector"]), // unión de literales
});
// el esquema es un VALOR que sabe validar en runtime.
// pero también lleva dentro toda la información de tipos...Un esquema Zod describe la forma (como un type) PERO también las reglas de validación
(no vacío, email válido, no negativo) —cosas que un tipo de TypeScript no puede expresar.
Y a diferencia de un type, el esquema es un VALOR real que existe en runtime y sabe
comprobar datos. Fíjate en z.string().email(): eso valida el FORMATO en ejecución,
algo imposible solo con tipos.
z.infer: el tipo sale del esquema
Aquí está la joya. Del esquema derivas el tipo de TypeScript automáticamente —una sola fuente de verdad para la validación y el tipado:
import { z } from "zod";
const UsuarioSchema = z.object({
id: z.number(),
nombre: z.string(),
rol: z.enum(["admin", "editor", "lector"]),
});
// deriva el tipo TS del esquema — NO lo escribes a mano:
type Usuario = z.infer<typeof UsuarioSchema>;
// = { id: number; nombre: string; rol: "admin" | "editor" | "lector" }
// usas Usuario como cualquier tipo, y está SIEMPRE sincronizado con el esquema:
function saludar(u: Usuario): string {
return "Hola, " + u.nombre + " (" + u.rol + ")";
}
// cambias el esquema (agregas un campo) → el tipo Usuario se actualiza SOLO.z.infer<typeof UsuarioSchema> extrae el tipo de TypeScript del esquema (usa el
ReturnType/typeof/conditional que aprendiste en los módulos 6 y 7, por dentro). El
resultado: defines la forma UNA vez —en el esquema— y obtienes la validación runtime Y
el tipo. Imposible que se desincronicen, porque son el mismo objeto. Este es el patrón
que hace a Zod imprescindible.
parse y safeParse: validar de verdad
El esquema valida datos con dos métodos, según cómo quieras manejar el fallo:
import { z } from "zod";
const UsuarioSchema = z.object({ id: z.number(), nombre: z.string() });
type Usuario = z.infer<typeof UsuarioSchema>;
// parse(): devuelve los datos tipados si son válidos, o LANZA si no:
try {
const usuario: Usuario = UsuarioSchema.parse(datosDelServidor);
// si llega aquí, 'usuario' es un Usuario GARANTIZADO (validado en runtime)
} catch (error) {
// ZodError con el detalle exacto de qué campo falló
}
// safeParse(): NO lanza, devuelve un resultado discriminado (módulo 4):
const resultado = UsuarioSchema.safeParse(datosDelServidor);
if (resultado.success) {
resultado.data.nombre; // 🔍 data es Usuario, seguro
} else {
resultado.error; // los errores de validación
}Fíjate en safeParse: devuelve { success: true; data: Usuario } | { success: false; error: ZodError } —¡una discriminated union (módulo 4)!—, así que la manejas con
narrowing sin try/catch. parse es más directo pero lanza. La diferencia con el
any de response.json(): aquí, si resultado.success es true, los datos están
VERIFICADOS en runtime, no solo prometidos.
El patrón completo: fetch + Zod
Todo junto —el cierre honesto del agujero de la lección anterior:
import { z } from "zod";
const GastoSchema = z.object({
id: z.number(),
nombre: z.string(),
valor: z.number().positive(),
});
type Gasto = z.infer<typeof GastoSchema>;
async function cargarGasto(url: string): Promise<Gasto> {
const respuesta = await fetch(url);
if (!respuesta.ok) throw new Error("HTTP " + respuesta.status);
const crudo: unknown = await respuesta.json(); // unknown, honesto
return GastoSchema.parse(crudo); // valida en runtime → Gasto GARANTIZADO
// si el servidor mandó basura, esto LANZA aquí, cerca del problema —
// no explota más tarde con un 'undefined' misterioso.
}La salida de validar un dato malo es clara y temprana:
cargarGasto con { nombre: 'Café', valor: -5 }
ZodError: [
{ path: ['id'], message: 'Required' },
{ path: ['valor'], message: 'Number must be greater than 0' }
]
¿Cuál es la ventaja de z.infer<typeof Schema> frente a escribir el type Usuario a mano y validar con un type guard separado?
Mini-reto
Diseña con esquema (en un proyecto con pnpm add zod): 1) crea un GastoSchema con
nombre (string no vacío), valor (número positivo) y categoria (enum de tres
opciones); 2) deriva type Gasto = z.infer<typeof GastoSchema>; 3) usa safeParse
sobre un dato válido y uno inválido (valor negativo), manejando el resultado con
narrowing (if (resultado.success)). Siente cómo el esquema es la única fuente de la
forma, la validación y el tipo.
Qué sigue
Has tipado el DOM y los datos externos. La próxima lección aborda un caso más: usar
librerías que NO traen tipos, o describir tipos que existen solo en runtime, con los
declaration files (.d.ts) —la forma de darle a TypeScript conocimiento sobre código
que no ve.