Zod describe cómo se ve un dato válido en tiempo de ejecución e infiere el mismo tipo de TypeScript que tu IDE autocompletar. En móvil, eso importa el doble: TextInput siempre devuelve cadenas, y los usuarios esperan errores en línea bajo el campo que acabas de dejar, no una ventana modal después de tocar Enviar.
// lib/schemas/profile.ts - compartido entre formulario y cliente de APIimport { z } from "zod";export const ProfileSchema = z.object({ displayName: z .string() .trim() .min(1, "El nombre de usuario es obligatorio") .max(40, "Mantenlo bajo 40 caracteres"), bio: z .string() .max(280, "La biografía debe tener 280 caracteres o menos") .optional() .or(z.literal("")), age: z .string() .min(1, "La edad es obligatoria") .pipe( z.coerce .number({ invalid_type_error: "Introduce un número" }) .int("Solo números enteros") .min(13, "Debes tener al menos 13 años") .max(120, "Introduce una edad realista") ), newsletter: z.boolean().default(false),});export type ProfileValues = z.infer<typeof ProfileSchema>;export const ProfileUpdateSchema = ProfileSchema.pick({ displayName: true, bio: true, newsletter: true,});export type ProfileUpdateValues = z.infer<typeof ProfileUpdateSchema>;
import { z } from "zod";const OrderSchema = z.object({ quantity: z.coerce.number().positive(), note: z.string().optional(),});type OrderInput = z.input<typeof OrderSchema>; // antes de transforms/coercetype OrderOutput = z.output<typeof OrderSchema>; // después - igual que z.infer cuando no hay transformstype OrderValues = z.infer<typeof OrderSchema>;
Instala con npx expo install zod @hookform/resolvers react-hook-form.
Exporta Schema, type Values = z.infer<typeof Schema>, y helpers safeParse de un módulo.
Reutiliza esquemas de respuesta de API con .pick() / .omit() / .extend() para que las formas de lista, detalle y formulario se mantengan alineadas.
Formularios de cumplimiento críticos de accesibilidad
Banner resumen
Mensaje errors.root de raíz
Fallos de servidor que no son de campo
// Mostrar un error de servidor que no es de camposetError("root", { type: "server", message: "Red no disponible. Intenta de nuevo." });{errors.root && <Text style={styles.banner}>{errors.root.message}</Text>}
Duplicar tipos y esquemas - Mantener una interface Profile separada de ProfileSchema se desvía en el primer cambio de API. Solución: Exporta solo z.infer<typeof ProfileSchema>.
Validar números sin coerción - z.number() falla cuando TextInput entrega "25" como una cadena. Solución:z.coerce.number() o z.string().regex(/^\d+$/).transform(Number).
.optional() vs cadena vacía - Campo bio opcional con "" falla z.string().email() encadenado después. Solución: Recorta y transforma "" - undefined, o usa .or(z.literal("")).
Mostrar errores antes de tocar - Validar onChange en cada pulsación de tecla frustra a los usuarios móviles. Solución:mode: "onTouched" y renderiza isTouched && error.
Coincidencia de contraseña en ruta incorrecta - .refine() sin path pone el error en la raíz _form. Solución:path: ["confirmPassword"] para que el mensaje se encuentre bajo el campo correcto.
Campos de API que aceptan valores nulos en formularios - Backend envía middleName: null pero el formulario usa "". Solución: Normaliza en defaultValues y esquema con .nullable().transform(v => v ?? "").
Esquema monolítico gigante - Un esquema de 40 campos hace que las rutas de error sean difíciles de leer. Solución: Compone esquemas de paso y fusiona con .merge() para asistentes, o valida por paso con .pick().