Diseñar Interfaces Públicas para Librerías

Lectura
15 min~5 min lectura

Concepto clave

Diseñar interfaces públicas para librerías en TypeScript es como crear el plano arquitectónico de un edificio que otros desarrolladores usarán. La interfaz pública define cómo los usuarios interactúan con tu código, mientras que la implementación interna permanece oculta. En TypeScript avanzado, esto implica usar tipos genéricos, tipos condicionales y tipos mapeados para crear APIs flexibles pero seguras.

Imagina que construyes una librería de validación de formularios. Los usuarios solo necesitan saber cómo llamar a las funciones de validación y qué errores pueden recibir, no cómo se implementa cada regla internamente. Una buena interfaz pública actúa como un contrato: establece expectativas claras, reduce errores en tiempo de compilación y facilita el mantenimiento a largo plazo. En arquitecturas complejas, este diseño es crucial para la escalabilidad y la colaboración en equipos grandes.

Cómo funciona en la práctica

Vamos a construir una interfaz pública para una librería de caché tipada. Empezamos definiendo tipos genéricos que permitan flexibilidad sin perder seguridad.

// Definición de tipos base
type CacheConfig = {
  maxSize: number;
  defaultTTL: number;
  serialize?: (value: T) => string;
  deserialize?: (data: string) => T;
};

// Interfaz pública principal
interface TypedCache {
  set(key: K, value: V, ttl?: number): void;
  get(key: K): V | undefined;
  delete(key: K): boolean;
  clear(): void;
  has(key: K): boolean;
  keys(): K[];
  values(): V[];
}

Ahora, implementamos una clase que exponga solo esta interfaz, ocultando los detalles internos como el almacenamiento en memoria o la lógica de expiración. Usamos tipos condicionales para métodos opcionales:

type CacheMethod = T extends { serialize: infer S } ? S : never;

class MemoryCache implements TypedCache {
  private storage: Map;
  private config: CacheConfig;

  constructor(config: CacheConfig) {
    this.storage = new Map();
    this.config = config;
  }

  set(key: K, value: V, ttl?: number): void {
    const expires = Date.now() + (ttl || this.config.defaultTTL);
    this.storage.set(key, { value, expires });
  }

  get(key: K): V | undefined {
    const item = this.storage.get(key);
    if (!item || item.expires < Date.now()) {
      this.storage.delete(key);
      return undefined;
    }
    return item.value;
  }

  // Otros métodos implementados aquí
}

Caso de estudio

Considera una librería para manejar solicitudes HTTP con tipado fuerte. Queremos que los usuarios puedan definir tipos para las respuestas y parámetros, pero la interfaz debe ser intuitiva.

Una interfaz bien diseñada reduce los bugs en producción en hasta un 40%, según estudios de proyectos open-source.
// Interfaz pública
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';

type RequestOptions = {
  params?: TParams;
  body?: TBody;
  headers?: Record;
};

interface TypedHttpClient {
  request(
    method: HttpMethod,
    url: string,
    options?: RequestOptions
  ): Promise;
}

// Uso por el consumidor
type UserResponse = { id: number; name: string; email: string };
type UserParams = { userId: number };

const client: TypedHttpClient = new HttpClient();
const user = await client.request('GET', '/api/user', {
  params: { userId: 123 }
});
// user está tipado como UserResponse, evitando errores comunes

Esta interfaz usa tipos genéricos para TResponse, TParams y TBody, permitiendo a los usuarios especificar tipos concretos mientras mantienen la flexibilidad. La implementación interna maneja la lógica de red, pero la interfaz pública solo expone lo necesario.

Errores comunes

  • Exponer detalles de implementación: Incluir métodos internos en la interfaz pública, lo que acopla a los usuarios a cambios futuros. Solución: Usa clases privadas o módulos internos.
  • Tipos demasiado amplios: Usar any o unknown en lugares donde tipos específicos mejorarían la seguridad. Solución: Aplica tipos genéricos con restricciones (T extends SomeType).
  • Falta de documentación de tipos: No documentar cómo los tipos afectan el comportamiento, llevando a malentendidos. Solución: Usa comentarios JSDoc o archivos d.ts separados.
  • Sobrecarga de opciones: Crear interfaces con docenas de parámetros opcionales, dificultando el uso. Solución: Agrupa opciones en objetos tipados y usa valores por defecto.
  • Ignorar la ergonomía del desarrollador: Diseñar interfaces complejas que requieren mucho boilerplate. Solución: Proporciona funciones helper y tipos utilitarios (Partial, Pick).

Checklist de dominio

  1. ¿La interfaz pública oculta todos los detalles de implementación innecesarios?
  2. ¿Usas tipos genéricos para permitir flexibilidad sin sacrificar seguridad de tipos?
  3. ¿Los métodos y propiedades tienen nombres claros y descriptivos?
  4. ¿Incluyes tipos para errores y casos límite en la interfaz?
  5. ¿Has probado la interfaz con ejemplos del mundo real para validar su usabilidad?
  6. ¿Documentas los tipos y su comportamiento en comentarios o archivos separados?
  7. ¿La interfaz es compatible con versiones anteriores cuando sea posible?

Diseña una interfaz pública para una librería de logging tipada

Crea una interfaz pública en TypeScript para una librería de logging que permita a los usuarios registrar mensajes con diferentes niveles de severidad y metadatos tipados. Sigue estos pasos:

  1. Define un tipo LogLevel con los valores: 'debug', 'info', 'warn', 'error', 'fatal'.
  2. Crea una interfaz TypedLogger con métodos para cada nivel de log (ej: debug, info, etc.) que acepten un mensaje string y un objeto opcional de metadatos.
  3. Usa tipos genéricos para permitir que los metadatos sean tipados por el usuario, pero con un valor por defecto de Record.
  4. Añade un método child que cree un nuevo logger con metadatos adicionales, usando tipos mapeados para combinar los tipos.
  5. Implementa una clase ConsoleLogger que implemente esta interfaz, registrando en la consola, pero no expongas los detalles internos en la interfaz.
  6. Escribe un ejemplo de uso que muestre cómo un consumidor podría usar tu logger con tipos personalizados.
Pistas
  • Usa tipos condicionales para manejar los metadatos opcionales en el método child.
  • Considera usar un tipo genérico con restricción para los metadatos, como T extends Record.
  • Recuerda que la interfaz debe ser fácil de usar; prueba con un ejemplo simple antes de añadir complejidad.

Evalua tu comprension

Completa el quiz interactivo de arriba para ganar XP.