Zod 4 - La Evolución de la Validación de Esquemas en TypeScript

2024.12.24

Qué es Zod

Zod es una biblioteca de declaración y validación de esquemas con TypeScript como prioridad. Con solo definir un esquema, puedes realizar validación e inferencia de tipos simultáneamente.

Nuevas Características de Zod 4

Mejoras de Rendimiento

Velocidad de validación:
- Zod 3: 100,000 ops/seg
- Zod 4: 300,000 ops/seg (3 veces más rápido)

Velocidad de parseo:
- Hasta 5 veces más rápido con esquemas complejos

Nueva API de Metadatos

import { z } from 'zod';

const userSchema = z.object({
  name: z.string().describe('Nombre del usuario'),
  email: z.string().email().describe('Correo electrónico'),
  age: z.number().min(0).max(150).describe('Edad')
}).describe('Información del usuario');

// Obtener metadatos
const metadata = userSchema.description;
const fields = userSchema.shape;

Uso Básico

Definición de Esquemas

import { z } from 'zod';

// Tipos primitivos
const stringSchema = z.string();
const numberSchema = z.number();
const booleanSchema = z.boolean();
const dateSchema = z.date();

// Objetos
const userSchema = z.object({
  id: z.number(),
  name: z.string(),
  email: z.string().email(),
  age: z.number().int().positive().optional(),
  role: z.enum(['user', 'admin']),
  createdAt: z.date().default(() => new Date())
});

// Inferencia de tipos
type User = z.infer<typeof userSchema>;
// {
//   id: number;
//   name: string;
//   email: string;
//   age?: number;
//   role: 'user' | 'admin';
//   createdAt: Date;
// }

Validación

// parse: Lanza una excepción en caso de fallo
const user = userSchema.parse({
  id: 1,
  name: 'Alice',
  email: 'alice@example.com',
  role: 'user'
});

// safeParse: Devuelve el resultado como objeto
const result = userSchema.safeParse(input);
if (result.success) {
  console.log(result.data);
} else {
  console.error(result.error.issues);
}

Transformación (Transform)

const schema = z.string()
  .transform(val => val.toLowerCase())
  .transform(val => val.trim());

const cleanSchema = z.object({
  price: z.string().transform(val => parseFloat(val)),
  quantity: z.string().transform(val => parseInt(val, 10))
});

Características Avanzadas

Refinamiento

const passwordSchema = z.string()
  .min(8, 'La contraseña debe tener al menos 8 caracteres')
  .refine(
    (val) => /[A-Z]/.test(val),
    { message: 'Debe incluir una letra mayúscula' }
  )
  .refine(
    (val) => /[0-9]/.test(val),
    { message: 'Debe incluir un número' }
  );

// superRefine (devuelve múltiples errores)
const formSchema = z.object({
  password: z.string(),
  confirmPassword: z.string()
}).superRefine((data, ctx) => {
  if (data.password !== data.confirmPassword) {
    ctx.addIssue({
      code: z.ZodIssueCode.custom,
      message: 'Las contraseñas no coinciden',
      path: ['confirmPassword']
    });
  }
});

Uniones y Discriminadores

// Unión normal
const responseSchema = z.union([
  z.object({ status: z.literal('success'), data: z.any() }),
  z.object({ status: z.literal('error'), message: z.string() })
]);

// Unión discriminada (más rápida)
const eventSchema = z.discriminatedUnion('type', [
  z.object({ type: z.literal('click'), x: z.number(), y: z.number() }),
  z.object({ type: z.literal('scroll'), offset: z.number() }),
  z.object({ type: z.literal('keypress'), key: z.string() })
]);

Esquemas Recursivos

interface Category {
  name: string;
  subcategories: Category[];
}

const categorySchema: z.ZodType<Category> = z.lazy(() =>
  z.object({
    name: z.string(),
    subcategories: z.array(categorySchema)
  })
);

Parcial y Requerido

const userSchema = z.object({
  id: z.number(),
  name: z.string(),
  email: z.string()
});

// Hacer todo opcional
const partialUser = userSchema.partial();

// Solo campos específicos opcionales
const updateUser = userSchema.partial({
  name: true,
  email: true
});

// Convertir opcionales en requeridos
const requiredUser = userSchema.required();

Validación de API

Express

import express from 'express';
import { z } from 'zod';

const createUserSchema = z.object({
  body: z.object({
    name: z.string(),
    email: z.string().email()
  }),
  query: z.object({
    notify: z.string().optional()
  })
});

function validate<T extends z.ZodType>(schema: T) {
  return (req: Request, res: Response, next: NextFunction) => {
    const result = schema.safeParse(req);
    if (!result.success) {
      return res.status(400).json({ errors: result.error.issues });
    }
    next();
  };
}

app.post('/users', validate(createUserSchema), (req, res) => {
  // Ya validado
});

tRPC

import { z } from 'zod';
import { publicProcedure, router } from './trpc';

export const appRouter = router({
  createUser: publicProcedure
    .input(z.object({
      name: z.string().min(1),
      email: z.string().email()
    }))
    .mutation(async ({ input }) => {
      return db.user.create({ data: input });
    })
});

Integración con Bibliotecas de Formularios

React Hook Form

import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';

const schema = z.object({
  name: z.string().min(1, 'El nombre es requerido'),
  email: z.string().email('Ingresa un correo electrónico válido')
});

function Form() {
  const { register, handleSubmit, formState: { errors } } = useForm({
    resolver: zodResolver(schema)
  });

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register('name')} />
      {errors.name && <span>{errors.name.message}</span>}
      {/* ... */}
    </form>
  );
}

Resumen

Zod 4 ha hecho que la validación de esquemas en TypeScript sea aún más rápida y fácil de usar. Con su integración de inferencia de tipos, ricas funciones de validación y compatibilidad con frameworks, es una herramienta indispensable para el desarrollo de aplicaciones type-safe.

← Volver a la lista