Tipar Respuestas de API con Genéricos y Utility Types

Lectura
20 min~4 min lectura

Concepto clave

Tipar respuestas de API con genéricos y utility types es una técnica avanzada que transforma el desarrollo frontend de arquitecturas complejas. En lugar de usar tipos estáticos o any, creas contratos de tipo flexibles que se adaptan dinámicamente a la estructura de datos devuelta por el backend. Esto es similar a cómo un arquitecto diseña planos modulares: defines un esqueleto genérico (el patrón de respuesta) y luego lo especializas para cada endpoint específico.

Los utility types como Partial, Pick, Omit y Record te permiten manipular tipos existentes sin duplicar código. Por ejemplo, Partial<T> hace todas las propiedades de T opcionales, útil para respuestas de API que pueden devolver datos incompletos en ciertos escenarios. Combinar esto con genéricos crea un sistema de tipos que escala con tu aplicación, reduciendo errores en tiempo de compilación y mejorando la autocompletación en tu IDE.

"Tipar APIs no es solo sobre validación; es sobre documentación viva que tu equipo puede usar para construir features más rápido y con menos bugs." — Experiencia de un Frontend Engineer en una startup de scale-up.

Cómo funciona en la práctica

Imagina que estás construyendo un dashboard de analytics con múltiples endpoints. Primero, defines un tipo base para respuestas de API:

interface ApiResponse<T> {
  data: T;
  status: number;
  message: string;
}

Luego, usas utility types para adaptar tipos de dominio. Supón que tienes un tipo User:

interface User {
  id: string;
  name: string;
  email: string;
  role: 'admin' | 'user';
  createdAt: Date;
}

Para un endpoint que devuelve una lista de usuarios con solo id y name (por ejemplo, para un dropdown), usas Pick:

type UserListResponse = ApiResponse<Pick<User, 'id' | 'name'>[]>;

Para otro endpoint que actualiza un usuario y acepta propiedades parciales, usas Partial:

type UpdateUserRequest = Partial<Omit<User, 'id' | 'createdAt'>>;

Esto asegura que el frontend solo envíe campos modificables, evitando errores comunes como intentar cambiar un id.

Caso de estudio

En una aplicación de e-commerce con arquitectura microservicios, cada servicio (users, products, orders) devuelve respuestas similares pero con datos distintos. Usamos genéricos para crear un cliente de API reutilizable:

async function fetchApi<T>(endpoint: string): Promise<ApiResponse<T>> {
  const response = await fetch(`https://api.example.com/${endpoint}`);
  const data: ApiResponse<T> = await response.json();
  return data;
}

Luego, tipamos respuestas específicas. Para el servicio de productos:

interface Product {
  id: string;
  name: string;
  price: number;
  category: string;
  inStock: boolean;
}

type ProductsResponse = ApiResponse<Product[]>;

// Uso en componente
const getProducts = async (): Promise<ProductsResponse> => {
  return fetchApi<Product[]>('products');
};

Para un endpoint de búsqueda que devuelve productos con campos opcionales, usamos Partial en el tipo de respuesta, reflejando que algunos campos pueden faltar en resultados parciales.

Errores comunes

  • Usar any en respuestas de API: Pierdes todos los beneficios de TypeScript. En su lugar, define tipos mínimos con unknown y haz type guards.
  • Duplicar tipos para endpoints similares: Crea tipos base y usa utility types para variaciones, evitando inconsistencia.
  • Ignorar estados de error en el tipado: Tipa tanto respuestas exitosas como errores, por ejemplo, con un tipo union ApiResponse<T> | ApiError.
  • No alinear tipos frontend-backend: Usa herramientas como OpenAPI o protobufs para generar tipos automáticamente desde contratos de API.
  • Olvidar tipar respuestas anidadas: Para datos complejos (ej., usuario con órdenes), usa tipos recursivos o Record para mapeos.

Checklist de dominio

  1. ¿Puedes definir un tipo genérico ApiResponse<T> que cubra data, status y message?
  2. ¿Sabes usar al menos tres utility types (Partial, Pick, Omit) en respuestas de API?
  3. ¿Has implementado un cliente de API con genéricos que se reutilice en múltiples endpoints?
  4. ¿Puedes tipar respuestas con datos anidados (ej., listas de objetos con relaciones)?
  5. ¿Entiendes cómo evitar la duplicación de tipos entre frontend y backend?
  6. ¿Has manejado errores de API con tipos específicos (no solo catch genérico)?
  7. ¿Puedes explicar la diferencia entre Record<string, T> y un array para respuestas de diccionario?

Construye un cliente de API tipado para un sistema de blog

En este ejercicio, crearás un cliente de API tipado para un sistema de blog con posts y comentarios. Sigue estos pasos:

  1. Define los tipos de dominio: Post con id, title, content, authorId, y Comment con id, text, postId.
  2. Crea un tipo genérico ApiResponse<T> que incluya data (de tipo T), status (number), y timestamp (string).
  3. Implementa una función fetchBlogApi<T>(endpoint: string) que devuelva una Promise<ApiResponse<T>>, simulando una llamada API con setTimeout y datos mock.
  4. Tipa respuestas específicas:
    • Para /posts, devuelve un array de Post.
    • Para /posts/:id, devuelve un Post con sus comentarios anidados (usa un tipo combinado).
    • Para /comments, devuelve solo id y text usando Pick.
  5. Escribe un ejemplo de uso que llame a estos endpoints y maneje los tipos correctamente.
Pistas
  • Usa Partial para simular datos incompletos en respuestas de edición.
  • Considera usar Record<string, Comment[]> para mapear comentarios por postId.
  • Asegúrate de que tu función fetchBlogApi use genéricos para inferir el tipo de data.

Evalua tu comprension

Completa el quiz interactivo de arriba para ganar XP.