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.
Receta
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.
Ejemplo Funcional
"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:
- Esquema único como fuente de verdad para múltiples tipos derivados
z.inferpara el tipo de salida validadoz.inputpara el tipo de entrada bruta (antes de valores por defecto y transformaciones)- Derivación de esquemas con
.omit(),.partial(),.required()
Análisis Profundo
Cómo Funciona
z.infer<typeof Schema>extrae el tipo TypeScript queparse()devuelve (el tipo de salida)z.input<typeof Schema>extrae el tipo queparse()acepta (antes de transformaciones y valores por defecto)- Cuando un esquema no tiene transformaciones o valores por defecto,
z.inputyz.outputson idénticos - Las transformaciones, valores por defecto,
.optional(),.nullable()pueden todos causar que entrada y salida difieran - Los métodos de derivación de esquemas (
.pick(),.omit(),.extend(),.partial()) producen nuevos esquemas con tipos derivados correctamente
Variaciones
Derivar 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);
// ...
}Notas de TypeScript
// 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>;Trampas
-
z.inferes salida, no entrada — Si tu esquema tiene transformaciones,z.inferte da el tipo post-transformación. Tu estado de formulario y cuerpo de solicitud de API necesitanz.input. Solución: Usaz.inputpara datos de formulario y formas de solicitud,z.infer(oz.output) para resultados validados. -
Los valores por defecto son invisibles para
z.infer—z.string().default("hi")se deduce comostringenz.infer, nostring | undefined. Peroz.inputmuestra correctamente comostring | undefined. Solución: Sé consciente de la asimetría cuando construyas objetos de entrada. -
No puedes usar
z.infersintypeof—z.infer<UserSchema>es incorrecto. Solución: Siempre escribez.infer<typeof UserSchema>. -
Las referencias circulares necesitan anotación explícita — Los esquemas recursivos con
z.lazyno se pueden deducir automáticamente. Solución: Declara el tipo manualmente y anota el esquema:const S: z.ZodType<MyType> = z.lazy(...).
Alternativas
| 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 |
Preguntas Frecuentes
¿Cuál es la diferencia entre z.infer, z.input, y z.output?
z.inferes un alias paraz.output-- el tipo después de transformaciones y valores por defectoz.inputes el tipo antes de transformaciones y valores por defecto -- lo que pasas enparse()- Cuando un esquema no tiene transformaciones o valores por defecto, los tres son idénticos
¿Cuándo deberías usar z.input en lugar de z.infer?
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.
¿Cómo derivar tipos CRUD de un esquema base único?
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 })>;¿Cómo afecta .default() a z.input vs z.infer?
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.
¿Cómo causan las transformaciones que z.input y z.output difieran?
const S = z.string().transform((s) => new Date(s));
type SIn = z.input<typeof S>; // string
type SOut = z.output<typeof S>; // Date¿Cómo usar un tipo derivado en una firma de función?
const 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());
}¿Qué métodos de esquema producen nuevos esquemas con tipos derivados correctamente?
.pick(), .omit(), .extend(), .merge(), .partial(), .required(), y .deepPartial() todos devuelven nuevos esquemas cuyo tipo z.infer se actualiza automáticamente.
Trampa: ¿Por qué z.infer con un valor de esquema en lugar de su tipo produce un error de TypeScript?
Debes usar typeof: z.infer<typeof UserSchema>. El parámetro genérico espera el tipo de la variable del esquema, no el valor mismo.
Trampa: ¿Por qué z.infer no puede resolver un esquema recursivo hecho con z.lazy?
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) })
);¿Cómo construir un componente genérico que acepte cualquier esquema de objeto Zod?
function SchemaForm<T extends z.ZodObject<any>>({
schema,
onSubmit,
}: {
schema: T;
onSubmit: (data: z.infer<T>) => void;
}) {
const fields = Object.keys(schema.shape);
// ...
}TypeScript: ¿Cuáles son los accesores de tipo interno detrás de z.infer y z.input?
z.infer<T>se resuelve aT["_output"]z.input<T>se resuelve aT["_input"]- Estas son propiedades marcadas en cada instancia de
ZodType
TypeScript: ¿Cómo usas satisfies con un tipo derivado?
const 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.
Relacionado
- Zod Basics — fundamentos de definición de esquema
- Zod Types — todos los tipos disponibles
- Zod Transforms — transformaciones que afectan tipos de entrada/salida
- RHF + Zod — formularios tipados de esquemas derivados