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