Volver a TypeScript Intermedio

APIs Tipadas de Forma Segura con TypeScript

Comparte tipos entre frontend y backend para eliminar toda una clase de errores en tiempo de ejecución.

Zod — Validación en Tiempo de Ejecución + Inferencia de Tipos

import { z } from 'zod'; // Define el schema una vez — obtienes tipo Y validación const UsuarioSchema = z.object({ id: z.number().int().positive(), nombre: z.string().min(2).max(50), correo: z.string().email(), rol: z.enum(['admin', 'usuario', 'moderador']), edad: z.number().min(0).max(150).optional(), }); // Inferir el tipo del schema type Usuario = z.infer<typeof UsuarioSchema>; // Validar en tiempo de ejecución (respuesta de API, entrada de formulario) function parsearUsuario(datos: unknown): Usuario { return UsuarioSchema.parse(datos); // Lanza ZodError si es inválido } // Parseo seguro (retorna { success, data } | { success: false, error }) const resultado = UsuarioSchema.safeParse(datosDesconocidos); if (resultado.success) { console.log(resultado.data.nombre); // Usuario tipado ✅ } else { console.error(resultado.error.issues); }

tRPC — Seguridad de Tipos de Extremo a Extremo

tRPC genera un cliente con tipado seguro desde tu router de servidor — sin generación de código.

// server/router.ts import { initTRPC } from '@trpc/server'; import { z } from 'zod'; const t = initTRPC.create(); export const appRouter = t.router({ usuario: t.router({ obtenerPorId: t.procedure .input(z.object({ id: z.string() })) .query(async ({ input }) => { return await db.usuario.findUnique({ where: { id: input.id } }); }), crear: t.procedure .input(z.object({ nombre: z.string(), correo: z.string().email() })) .mutation(async ({ input }) => { return await db.usuario.create({ data: input }); }), }), }); export type AppRouter = typeof appRouter;
// client/api.ts import { createTRPCReact } from '@trpc/react-query'; import type { AppRouter } from '../server/router'; export const trpc = createTRPCReact<AppRouter>(); // Uso en un componente: function PaginaUsuario({ id }: { id: string }) { const { data: usuario } = trpc.usuario.obtenerPorId.useQuery({ id }); // `usuario` está completamente tipado — ¡el autocompletado funciona! return <div>{usuario?.nombre}</div>; }

Patrón Repositorio Genérico

interface Repositorio<T, ID> { buscarPorId(id: ID): Promise<T | null>; buscarTodos(): Promise<T[]>; crear(datos: Omit<T, 'id' | 'creadoEn'>): Promise<T>; actualizar(id: ID, datos: Partial<T>): Promise<T>; eliminar(id: ID): Promise<void>; } class RepositorioUsuario implements Repositorio<Usuario, string> { async buscarPorId(id: string) { return db.usuario.findUnique({ where: { id } }); } async buscarTodos() { return db.usuario.findMany(); } async crear(datos: Omit<Usuario, 'id' | 'creadoEn'>) { return db.usuario.create({ data: datos }); } async actualizar(id: string, datos: Partial<Usuario>) { return db.usuario.update({ where: { id }, data: datos }); } async eliminar(id: string) { await db.usuario.delete({ where: { id } }); } }

Tipos Marcados (Branded Types)

Evita pasar valores del mismo tipo primitivo en el lugar equivocado:

type Marca<T, B> = T & { __marca: B }; type IdUsuario = Marca<string, 'IdUsuario'>; type IdProducto = Marca<string, 'IdProducto'>; function crearIdUsuario(id: string): IdUsuario { return id as IdUsuario; } function obtenerUsuario(id: IdUsuario) { /* ... */ } function obtenerProducto(id: IdProducto) { /* ... */ } const uid = crearIdUsuario('usuario-123'); obtenerUsuario(uid); // ✅ // obtenerProducto(uid); // ❌ Error de TypeScript — IdUsuario ≠ IdProducto