Zod Infer
Deriva tipos de TypeScript de esquemas Zod con z.infer, z.input, y z.output - fuente única de verdad para validación en tiempo de ejecución y seguridad en tiempo de compilación.
Busca en todas las páginas de la documentación
Deriva tipos de TypeScript de esquemas Zod con z.infer, z.input, y z.output - fuente única de verdad para validación en tiempo de ejecución y seguridad en tiempo de compilación.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Tarjeta de referencia rápida - lista para copiar y pegar.
import { z } from "zod";
const UserSchema = z.object({
id: z.string().uuid(),
name: z.string().min(1),
email: z.string().email(),
role: z.enum(["admin", "editor", "viewer"]),
createdAt: z.string().datetime().transform((s) => new Date(s)),
});
// z.infer = z.output - el tipo DESPUÉS de transformaciones
type User = z.infer<typeof UserSchema>;
// { id: string; name: string; email: string; role: "admin" | "editor" | "viewer"; createdAt: Date }
// z.input - el tipo ANTES de transformaciones (lo que pasas)
type UserInput = z.input<typeof UserSchema>;
// { id: string; name: string; email: string; role: "admin" | "editor" | "viewer"; createdAt: string }
// z.output - lo mismo que z.infer
type UserOutput = z.output<typeof UserSchema>;Cuándo usarlo: Cada vez que tengas un esquema Zod y necesites un tipo TypeScript que coincida - para props, estado, payloads de API, filas de base de datos, o firmas de funciones.
"use client";
import { useState } from "react";
import { z } from "zod";
// El esquema es la fuente única de verdad
const TodoSchema = z.object({
id: z.string().uuid(),
title: z.string().min(1, "Título requerido").max(200),
completed: z.boolean().default(false),
priority: z.coerce.number().int().min(1).max(5).default(3),
tags: z.array(z.string()).default([]),
});
// Deriva todos los tipos del esquema
type Todo = z.infer<typeof TodoSchema>;
type TodoInput = z.input<typeof TodoSchema>;
// Esquema para crear - omite campos auto-generados
const CreateTodoSchema = TodoSchema.omit({ id: true });
type CreateTodoInput = z.input<typeof CreateTodoSchema>;
// Esquema para actualizar - todo parcial excepto id
const UpdateTodoSchema = TodoSchema.partial().required({ id: true });
type UpdateTodo = z.infer<typeof UpdateTodoSchema>;
export function TodoManager() {
const [todos, setTodos] = useState<Todo[]>([]);
const [error, setError] = useState("");
function addTodo(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
const fd = new FormData(e.currentTarget);
const raw: CreateTodoInput = {
title: fd.get("title") as string,
completed: false,
priority: fd.get("priority") as unknown as number,
tags: (fd.get("tags") as string).split(",").filter(Boolean).map((t) => t.trim()),
};
const result = CreateTodoSchema.safeParse(raw);
if (!result.success) {
setError(result.error.issues.map((i) => i.message).join(", "));
return;
}
const newTodo: Todo = { ...result.data, id: crypto.randomUUID() };
setTodos((prev) => [...prev, newTodo]);
setError("");
e.currentTarget.reset();
}
return (
<div className="max-w-md space-y-4">
<form onSubmit={addTodo} className="flex flex-col gap-2">
<input name="title" placeholder="Título del pendiente" className="rounded border p-2" />
<input name="priority" type="number" min={1} max={5} defaultValue={3} className="rounded border p-2" />
<input name="tags" placeholder="Etiquetas (separadas por comas)" className="rounded border p-2" />
<button type="submit" className="rounded bg-blue-600 px-4 py-2 text-white">Agregar</button>
{error && <p className="text-sm text-red-600">{error}</p>}
</form>
<ul className="space-y-1">
{todos.map((t) => (
<li key={t.id} className="rounded border p-2 text-sm">
<strong>{t.title}</strong> - Prioridad: {t.priority} - Etiquetas: {t.tags.join(", ") || "ninguna"}
</li>
))}
</ul>
</div>
);
}Lo que esto demuestra:
z.infer para el tipo de salida validadoz.input para el tipo de entrada bruta (antes de valores por defecto y transformaciones).omit(), .partial(), .required()z.infer<typeof Schema> extrae el tipo TypeScript que parse() devuelve (el tipo de salida)z.input<typeof Schema> extrae el tipo que parse() acepta (antes de transformaciones y valores por defecto)z.input y z.output son idénticos.optional(), .nullable() pueden todos causar que entrada y salida difieran.pick(), .omit(), .extend(), .partial()) producen nuevos esquemas con tipos derivados correctamenteDerivar tipos CRUD de un único esquema:
const ItemSchema = z.object({
id: z.string().uuid(),
name: z.string(),
price: z.number().positive(),
updatedAt: z.date(),
});
type Item = z.infer<typeof ItemSchema>;
type CreateItem = z.infer<typeof ItemSchema.omit({ id: true, updatedAt: true })>;
type UpdateItem = z.infer<typeof ItemSchema.partial().required({ id: true })>;
type ItemSummary = z.infer<typeof ItemSchema.pick({ id: true, name: true })>;Usar tipos derivados en firmas de funciones:
const ApiResponse = z.object({
data: z.array(UserSchema),
total: z.number(),
page: z.number(),
});
type ApiResponse = z.infer<typeof ApiResponse>;
async function fetchUsers(page: number): Promise<ApiResponse> {
const res = await fetch(`/api/users?page=${page}`);
return ApiResponse.parse(await res.json());
}Componentes genéricos basados en esquemas:
function SchemaForm<T extends z.ZodObject<any>>({
schema,
onSubmit,
}: {
schema: T;
onSubmit: (data: z.infer<T>) => void;
}) {
// Construir campos de formulario desde schema.shape
const fields = Object.keys(schema.shape);
// ...
}// z.infer es un alias de tipo para z.output
type Infer<T extends z.ZodType> = T["_output"];
type Input<T extends z.ZodType> = T["_input"];
type Output<T extends z.ZodType> = T["_output"];
// Los valores por defecto hacen que los campos sean opcionales en entrada pero requeridos en salida
const S = z.object({ count: z.number().default(0) });
type SIn = z.input<typeof S>; // { count?: number | undefined }
type SOut = z.output<typeof S>; // { count: number }
// Usar satisfies con tipos derivados para constantes type-safe
const defaultUser = {
name: "Guest",
email: "guest@example.com",
role: "viewer" as const,
} satisfies Partial<User>;z.infer es salida, no entrada - Si tu esquema tiene transformaciones, z.infer te da el tipo post-transformación. Tu estado de formulario y cuerpo de solicitud de API necesitan z.input. Solución: Usa z.input para datos de formulario y formas de solicitud, z.infer (o z.output) para resultados validados.
Los valores por defecto son invisibles para z.infer - z.string().default("hi") se deduce como string en z.infer, no string | undefined. Pero z.input muestra correctamente como string | undefined. Solución: Sé consciente de la asimetría cuando construyas objetos de entrada.
No puedes usar z.infer sin typeof - z.infer<UserSchema> es incorrecto. Solución: Siempre escribe z.infer<typeof UserSchema>.
Las referencias circulares necesitan anotación explícita - Los esquemas recursivos con z.lazy no se pueden deducir automáticamente. Solución: Declara el tipo manualmente y anota el esquema: const S: z.ZodType<MyType> = z.lazy(...).
| Alternativa | Úsalo Cuando | No lo Uses Cuando |
|---|---|---|
| Interfaces manuales de TypeScript | No tienes necesidades de validación en tiempo de ejecución | Quieres una fuente única de verdad para tipos y validación |
| TypeBox | Necesitas salida JSON Schema junto a tipos TypeScript | No necesitas interoperabilidad JSON Schema |
Valibot InferOutput | Usas Valibot y quieres el mismo patrón | Estás estandarizado en Zod |
| inferencia de tRPC desde routers | Tus tipos fluyen a través de tRPC de extremo a extremo | Necesitas validación autónoma fuera de tRPC |
z.infer es un alias para z.output -- el tipo después de transformaciones y valores por defectoz.input es el tipo antes de transformaciones y valores por defecto -- lo que pasas en parse()Usa z.input para estado de formulario, cuerpos de solicitud, y cualquier código que construya los datos brutos antes de analizar. Usa z.infer (salida) para el resultado validado después de analizar.
const Item = z.object({ id: z.string(), name: z.string(), price: z.number() });
type CreateItem = z.infer<typeof Item.omit({ id: true })>;
type UpdateItem = z.infer<typeof Item.partial().required({ id: true })>;
type ItemSummary = z.infer<typeof Item.pick({ id: true, name: true })>;const S = z.object({ count: z.number().default(0) });
type SIn = z.input<typeof S>; // { count?: number | undefined }
type SOut = z.output<typeof S>; // { count: number }El campo es opcional en entrada pero garantizado en salida.
const S = z.string().transform((s) => new Date(s));
type SIn = z.input<typeof S>; // string
type SOut = z.output<typeof S>; // Dateconst ApiResponse = z.object({ data: z.array(UserSchema), total: z.number() });
type ApiResponse = z.infer<typeof ApiResponse>;
async function fetchUsers(): Promise<ApiResponse> {
const res = await fetch("/api/users");
return ApiResponse.parse(await res.json());
}.pick(), .omit(), .extend(), .merge(), .partial(), .required(), y .deepPartial() todos devuelven nuevos esquemas cuyo tipo z.infer se actualiza automáticamente.
Debes usar typeof: z.infer<typeof UserSchema>. El parámetro genérico espera el tipo de la variable del esquema, no el valor mismo.
TypeScript no puede deducir tipos recursivos de z.lazy. Debes declarar el tipo manualmente y anotar el esquema:
type Category = { name: string; children: Category[] };
const CategorySchema: z.ZodType<Category> = z.lazy(() =>
z.object({ name: z.string(), children: z.array(CategorySchema) })
);function SchemaForm<T extends z.ZodObject<any>>({
schema,
onSubmit,
}: {
schema: T;
onSubmit: (data: z.infer<T>) => void;
}) {
const fields = Object.keys(schema.shape);
// ...
}z.infer<T> se resuelve a T["_output"]z.input<T> se resuelve a T["_input"]ZodTypeconst defaultUser = {
name: "Guest",
email: "guest@example.com",
role: "viewer" as const,
} satisfies Partial<User>;Esto verifica que el objeto coincide con el tipo sin ampliarlo.
Revisado por Chris St. John·Última actualización: 10 jul 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥