Tipos Zod
Domina cada tipo primitivo y compuesto de Zod - strings, números, arrays, objetos, enums y uniones.
Busca en todas las páginas de la documentación
Domina cada tipo primitivo y compuesto de Zod - strings, números, arrays, objetos, enums y uniones.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Tarjeta de receta de referencia rápida - lista para copiar y pegar.
import { z } from "zod";
// Primitivos
const str = z.string().min(1).max(255);
const num = z.number().int().positive();
const bool = z.boolean();
const date = z.date();
const bigint = z.bigint();
// Strings con validadores integrados
const email = z.string().email();
const url = z.string().url();
const uuid = z.string().uuid();
const cuid = z.string().cuid();
const regex = z.string().regex(/^[A-Z]{3}-\d{4}$/);
// Números
const price = z.number().min(0).max(999999).multipleOf(0.01);
const port = z.number().int().gte(1024).lte(65535);
// Arrays
const tags = z.array(z.string()).min(1).max(10);
const uniqueTags = z.array(z.string()).nonempty();
// Objetos
const User = z.object({
id: z.string().uuid(),
name: z.string(),
role: z.enum(["admin", "user", "guest"]),
});
// Enums
const Status = z.enum(["active", "inactive", "pending"]);
const NativeEnum = z.nativeEnum(MyEnum); // funciona con enums de TS
// Uniones y uniones discriminadas
const Result = z.discriminatedUnion("status", [
z.object({ status: z.literal("ok"), data: z.string() }),
z.object({ status: z.literal("error"), message: z.string() }),
]);
// Opcionales, nullables, valores por defecto
const optional = z.string().optional(); // string | undefined
const nullable = z.string().nullable(); // string | null
const nullish = z.string().nullish(); // string | null | undefined
const withDefault = z.string().default("N/A");Cuándo usarlo: Cuando necesites definir la forma y restricciones de cualquier estructura de datos para validación.
"use client";
import { useState } from "react";
import { z } from "zod";
const ProductSchema = z.object({
name: z.string().min(1, "Nombre de producto requerido").max(100),
price: z.coerce.number().positive("El precio debe ser positivo"),
category: z.enum(["electronics", "clothing", "food", "other"]),
tags: z.array(z.string().min(1)).min(1, "Al menos una etiqueta").max(5),
metadata: z
.object({
weight: z.coerce.number().positive().optional(),
color: z.string().optional(),
})
.optional(),
});
type Product = z.infer<typeof ProductSchema>;
export function ProductForm() {
const [result, setResult] = useState<string>("");
function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
const fd = new FormData(e.currentTarget);
const raw = {
name: fd.get("name"),
price: fd.get("price"),
category: fd.get("category"),
tags: (fd.get("tags") as string)?.split(",").map((t) => t.trim()),
metadata: {
weight: fd.get("weight") || undefined,
color: fd.get("color") || undefined,
},
};
const parsed = ProductSchema.safeParse(raw);
if (parsed.success) {
setResult(JSON.stringify(parsed.data, null, 2));
} else {
setResult(parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("\n"));
}
}
return (
<form onSubmit={handleSubmit} className="flex max-w-md flex-col gap-3">
<input name="name" placeholder="Nombre de producto" className="rounded border p-2" />
<input name="price" placeholder="Precio" type="number" step="0.01" className="rounded border p-2" />
<select name="category" className="rounded border p-2">
<option value="electronics">Electrónica</option>
<option value="clothing">Ropa</option>
<option value="food">Comida</option>
<option value="other">Otro</option>
</select>
<input name="tags" placeholder="Etiquetas (separadas por coma)" className="rounded border p-2" />
<input name="weight" placeholder="Peso (opcional)" type="number" className="rounded border p-2" />
<input name="color" placeholder="Color (opcional)" className="rounded border p-2" />
<button type="submit" className="rounded bg-blue-600 px-4 py-2 text-white">Validar</button>
{result && <pre className="rounded bg-gray-50 p-3 text-sm whitespace-pre-wrap">{result}</pre>}
</form>
);
}Lo que esto demuestra:
z.coerce.number() para conversión de string a número en formulariosz.enum()ZodType e implementa un método _parse internamentez.string().min(1).email() crea un nuevo esquema en cada pasoz.coerce.* invocan el constructor nativo (Number(), String(), Boolean()) antes de validarz.enum() acepta una tupla de solo lectura de literales de string e infiere un tipo de uniónz.discriminatedUnion() utiliza una clave compartida para despachar a la rama correcta eficientementez.object() es estricto respecto a la forma - usa .extend(), .merge(), .pick(), .omit() para derivar nuevas formasExtender y fusionar objetos:
const Base = z.object({ id: z.string().uuid(), createdAt: z.date() });
const WithName = Base.extend({ name: z.string() });
const Merged = Base.merge(z.object({ email: z.string().email() }));
const Picked = Base.pick({ id: true });
const Omitted = Base.omit({ createdAt: true });
const Partial = Base.partial(); // todos los campos opcionales
const Required = Partial.required(); // todos los campos requeridos nuevamente
const DeepPartial = Base.deepPartial(); // objetos anidados también opcionalesTipos Record y Map:
const Config = z.record(z.string(), z.number());
// { [key: string]: number }
const Scores = z.map(z.string(), z.number());
// Map<string, number>Tuplas y tipos literales:
const Coord = z.tuple([z.number(), z.number()]);
const Tagged = z.tuple([z.literal("point"), z.number(), z.number()]);
const WithRest = z.tuple([z.string()]).rest(z.number());
// [string, ...number[]]Esquemas lazy para tipos recursivos:
type Category = { name: string; children: Category[] };
const CategorySchema: z.ZodType<Category> = z.lazy(() =>
z.object({
name: z.string(),
children: z.array(CategorySchema),
})
);// z.enum produce un tipo de unión
const Role = z.enum(["admin", "user"]);
type Role = z.infer<typeof Role>; // "admin" | "user"
Role.enum.admin; // "admin" - acceso a constante tipada
Role.options; // ["admin", "user"] - la tupla
// nativeEnum funciona con enums de TypeScript
enum Direction { Up = "UP", Down = "DOWN" }
const DirSchema = z.nativeEnum(Direction);
type Dir = z.infer<typeof DirSchema>; // Direction
// Las uniones discriminadas se infieren correctamente
type Result = z.infer<typeof Result>;
// { status: "ok"; data: string } | { status: "error"; message: string }z.enum requiere as const - Si pasas un array simple, TypeScript amplía el tipo a string[]. Fix: Usa z.enum(["a", "b"] as const) o define el array con as const primero.
z.coerce.date() acepta números - z.coerce.date() invoca new Date(value), por lo que acepta timestamps y números arbitrarios. Fix: Si quieres solo strings ISO, usa z.string().datetime() en su lugar.
z.object no es z.record - z.object requiere claves exactas; z.record permite cualquier clave de un tipo dado. Mezclarlos genera validación incorrecta.
Union vs discriminatedUnion - z.union prueba cada rama secuencialmente y devuelve la primera coincidencia. z.discriminatedUnion utiliza un campo clave para despacho O(1) y proporciona mejores mensajes de error. Fix: Prefiere z.discriminatedUnion cuando tus objetos comparten un campo de tipo.
| Alternativa | Úsalo Cuando | No lo Uses Cuando |
|---|---|---|
| Guardias de tipo TypeScript | Estrechamiento de tiempo de ejecución simple sin una biblioteca | Necesitas esquemas declarativos con mensajes de error |
| io-ts | Quieres composición de codec estilo programación funcional | Prefieres una API más simple e imperativa |
| Superstruct | Necesitas validación basada en struct con coerción personalizada | Quieres validadores .email(), .url() integrados |
| JSON Schema (Ajv) | Necesitas interoperabilidad de esquema entre lenguajes | Quieres inferencia ajustada de TypeScript |
.email(), .url(), .uuid(), .cuid(), .regex(), .min(), .max(), .length(), .startsWith(), .endsWith(), .trim(), .datetime(), e .ip().
z.enum(["a", "b"]) acepta una tupla de literales de string e infiere un tipo de uniónz.nativeEnum(MyEnum) funciona con declaraciones de enum de TypeScriptz.enum para código nuevo; usa z.nativeEnum para enums de TS existentesz.union prueba cada rama secuencialmente y devuelve la primera coincidenciaz.discriminatedUnion utiliza un campo clave compartido para despacho O(1) y mejores mensajes de errorz.discriminatedUnion cuando objetos comparten un campo de tipo/statusconst Base = z.object({ id: z.string(), name: z.string() });
const Extended = Base.extend({ email: z.string().email() });
const Picked = Base.pick({ id: true });
const Omitted = Base.omit({ id: true });
const Partial = Base.partial();
const DeepPartial = Base.deepPartial();const Config = z.record(z.string(), z.number());
// Tipo inferido: { [key: string]: number }Esto es diferente de z.object(), que requiere claves exactas.
const WithRest = z.tuple([z.string()]).rest(z.number());
// Tipo inferido: [string, ...number[]]type Category = { name: string; children: Category[] };
const CategorySchema: z.ZodType<Category> = z.lazy(() =>
z.object({
name: z.string(),
children: z.array(CategorySchema),
})
);Debes declarar el tipo manualmente porque TypeScript no puede inferir tipos recursivos desde z.lazy.
z.coerce.number() invoca Number(value) en la entrada antes de validar. Esto convierte strings como "42" a 42. z.number() simple rechaza strings.
TypeScript amplía el tipo de array a string[], perdiendo los tipos literales. Fix usando as const:
const roles = ["admin", "user"] as const;
const Role = z.enum(roles); // "admin" | "user"z.coerce.date() invoca new Date(value), que acepta timestamps y números. new Date(999) es una fecha válida. Si quieres solo strings ISO, usa z.string().datetime() en su lugar.
.optional() permite undefined.nullable() permite null.default(value) hace que el campo sea opcional en la entrada pero proporciona un valor por defecto en la salidaconst Role = z.enum(["admin", "user"]);
Role.enum.admin; // "admin" (constante tipada)
Role.options; // ["admin", "user"] (la tupla)
type Role = z.infer<typeof Role>; // "admin" | "user"Produce un tipo de unión adecuado donde cada rama se estrecha por la clave discriminante:
type Result = { status: "ok"; data: string } | { status: "error"; message: string };TypeScript puede estrechar el tipo verificando el campo status.
Revisado por Chris St. John·Última actualización: 19 jul 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥