Tipos Zod
Domina cada tipo primitivo y compuesto de Zod — strings, números, arrays, objetos, enums y uniones.
Receta
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.
Ejemplo Funcional
"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:
- Coerción con
z.coerce.number()para conversión de string a número en formularios - Validación de enum con
z.enum() - Objetos anidados opcionales
- Validación de arrays desde entrada separada por comas
Profundización
Cómo Funciona
- Cada tipo de Zod extiende
ZodTypee implementa un método_parseinternamente - Los validadores se encadenan inmutablemente —
z.string().min(1).email()crea un nuevo esquema en cada paso - Las llamadas
z.coerce.*invocan el constructor nativo (Number(),String(),Boolean()) antes de validar z.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 formas
Variaciones
Extender 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),
})
);Notas de TypeScript
// 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 }Gotchas
-
z.enumrequiereas const— Si pasas un array simple, TypeScript amplía el tipo astring[]. Fix: Usaz.enum(["a", "b"] as const)o define el array conas constprimero. -
z.coerce.date()acepta números —z.coerce.date()invocanew Date(value), por lo que acepta timestamps y números arbitrarios. Fix: Si quieres solo strings ISO, usaz.string().datetime()en su lugar. -
z.objectno esz.record—z.objectrequiere claves exactas;z.recordpermite cualquier clave de un tipo dado. Mezclarlos genera validación incorrecta. -
Union vs discriminatedUnion —
z.unionprueba cada rama secuencialmente y devuelve la primera coincidencia.z.discriminatedUnionutiliza un campo clave para despacho O(1) y proporciona mejores mensajes de error. Fix: Prefierez.discriminatedUnioncuando tus objetos comparten un campo de tipo.
Alternativas
| 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 |
FAQs
¿Cuáles son los validadores de string integrados disponibles en Zod?
.email(), .url(), .uuid(), .cuid(), .regex(), .min(), .max(), .length(), .startsWith(), .endsWith(), .trim(), .datetime(), e .ip().
¿Cuál es la diferencia entre z.enum y z.nativeEnum?
z.enum(["a", "b"])acepta una tupla de literales de string e infiere un tipo de uniónz.nativeEnum(MyEnum)funciona con declaraciones deenumde TypeScript- Prefiere
z.enumpara código nuevo; usaz.nativeEnumpara enums de TS existentes
¿Cuál es la diferencia entre z.union y z.discriminatedUnion?
z.unionprueba cada rama secuencialmente y devuelve la primera coincidenciaz.discriminatedUnionutiliza un campo clave compartido para despacho O(1) y mejores mensajes de error- Prefiere
z.discriminatedUnioncuando objetos comparten un campo de tipo/status
¿Cómo extiendes o derivas nuevos esquemas de objeto desde uno existente?
const 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();¿Cómo defines un tipo record (claves dinámicas)?
const Config = z.record(z.string(), z.number());
// Tipo inferido: { [key: string]: number }Esto es diferente de z.object(), que requiere claves exactas.
¿Cómo defines una tupla con un elemento rest?
const WithRest = z.tuple([z.string()]).rest(z.number());
// Tipo inferido: [string, ...number[]]¿Cómo creas un esquema recursivo (por ejemplo, una estructura de árbol)?
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.
¿Qué hace z.coerce.number() diferente de z.number()?
z.coerce.number() invoca Number(value) en la entrada antes de validar. Esto convierte strings como "42" a 42. z.number() simple rechaza strings.
Gotcha: ¿Por qué z.enum falla cuando pasas una variable de array simple?
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"Gotcha: ¿Por qué z.coerce.date() acepta números arbitrarios como 999?
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.
¿Cuál es la diferencia entre .optional(), .nullable(), y .default()?
.optional()permiteundefined.nullable()permitenull.default(value)hace que el campo sea opcional en la entrada pero proporciona un valor por defecto en la salida
TypeScript: ¿Cómo z.enum proporciona acceso a constantes tipadas?
const 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"TypeScript: ¿Cómo z.discriminatedUnion infiere su tipo?
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.
Relacionado
- Zod Basics — parse, safeParse, manejo de errores
- Zod Transforms — transform, refine, preprocess
- Zod Infer — derivar tipos de TypeScript
- Server Action Forms — Zod en server actions