Ejercicio: Diseñar un Sistema de Notificaciones Tipado

Video
30 min~4 min lectura

Reproductor de video

Concepto clave

En arquitecturas complejas, los sistemas de notificaciones requieren un tipado estricto que garantice consistencia y seguridad en tiempo de compilación. Los patrones genéricos en TypeScript permiten crear abstracciones reutilizables que se adaptan a diferentes tipos de notificaciones sin perder información de tipos.

Imagina un sistema postal moderno: cada tipo de paquete (urgente, certificado, internacional) tiene requisitos específicos de etiquetado, seguimiento y entrega. Los genéricos actúan como plantillas inteligentes que aseguran que cada "paquete" de notificación cumpla con sus reglas específicas antes de enviarse.

Cómo funciona en la práctica

Comencemos con una interfaz base para notificaciones:

interface NotificationBase {
  type: T;
  timestamp: Date;
  priority: 'low' | 'medium' | 'high';
}

Ahora creamos un tipo genérico que extiende esta base:

type TypedNotification = NotificationBase & {
  payload: P;
  metadata: {
    source: string;
    version: number;
  };
};

// Implementación concreta
interface EmailPayload {
  recipient: string;
  subject: string;
  body: string;
  attachments?: string[];
}

type EmailNotification = TypedNotification<'email', EmailPayload>;

Este patrón permite crear notificaciones tipadas para diferentes canales (email, push, SMS) manteniendo consistencia estructural.

Caso de estudio

En una plataforma de e-commerce, necesitamos notificaciones para:

  1. Confirmación de pedido (email)
  2. Alerta de stock bajo (push)
  3. Recordatorio de carrito abandonado (SMS)

Implementamos un sistema centralizado:

class NotificationSystem {
  private notifications: TypedNotification[] = [];
  
  send(notification: TypedNotification): void {
    // Validación de tipos en tiempo de compilación
    this.notifications.push(notification);
    this.process(notification);
  }
  
  private process(notification: TypedNotification): void {
    switch(notification.type) {
      case 'email':
        // TypeScript sabe que payload es EmailPayload aquí
        const emailPayload = notification.payload as EmailPayload;
        console.log(`Enviando email a: ${emailPayload.recipient}`);
        break;
      case 'push':
        // Lógica específica para push
        break;
    }
  }
}
El tipado profundo previene errores como enviar un SMS con datos de email, detectándolo durante el desarrollo, no en producción.

Errores comunes

  • Any abuse: Usar any en genéricos destruye la seguridad de tipos. En lugar de TypedNotification, define interfaces específicas para cada payload.
  • Over-generification: Crear genéricos demasiado abstractos que pierden utilidad. Cada parámetro genérico debe tener una responsabilidad clara.
  • Type assertion incorrecta: Usar as sin validación previa. Implementa type guards para conversiones seguras.
  • Ignorar constraints: No definir límites (extends) en genéricos, permitiendo tipos incompatibles.
  • Mutable arrays tipados: Permitir modificación directa de arrays que deberían ser inmutables para mantener consistencia de tipos.

Checklist de dominio

  1. ¿Puedes crear un tipo genérico que acepte solo notificaciones con prioridad alta o media?
  2. ¿Implementaste type guards para diferenciar entre tipos de notificación en runtime?
  3. ¿Usas conditional types para transformar payloads basado en el tipo de notificación?
  4. ¿Aplicaste el principio de sustitución de Liskov en tu jerarquía de notificaciones?
  5. ¿Validaste que los genéricos mantengan covarianza/contravarianza donde sea necesario?
  6. ¿Documentaste con comentarios JSDoc los constraints de cada parámetro genérico?
  7. ¿Creaste tests que verifiquen el tipado en casos límite (edge cases)?

Diseña un sistema de notificaciones con tipos condicionales y mapeados

Objetivo

Crear un sistema de notificaciones tipado que use tipos condicionales de TypeScript para transformar automáticamente los payloads basado en el canal de envío.

Pasos

  1. Define una unión de tipos para los canales: 'email' | 'push' | 'sms' | 'in-app'
  2. Crea interfaces específicas para cada tipo de payload:
    • EmailPayload: con subject, body, attachments opcional
    • PushPayload: con title, body, deepLink
    • SMSPayload: con phoneNumber, message
    • InAppPayload: con userId, component, data
  3. Implementa un tipo condicional que mapee cada canal a su payload correspondiente:
    type PayloadForChannel = C extends 'email' ? EmailPayload :
      C extends 'push' ? PushPayload :
      C extends 'sms' ? SMSPayload :
      InAppPayload;
  4. Crea una clase NotificationDispatcher con un método genérico:
    send(channel: C, payload: PayloadForChannel): void
  5. Implementa validación que rechace combinaciones incorrectas (ej: enviar SMSPayload con channel 'email')
  6. Añade logging tipado que muestre propiedades específicas según el canal

Requisitos técnicos

  • Usa tipos mapeados para crear configuraciones por canal
  • Implementa al menos un tipo utilitario propio (ej: MakeOptional<T, K>)
  • Incluye manejo de errores con tipos específicos
  • Exporta tipos para uso en otros módulos
Pistas
  • Usa 'extends' en genéricos para constraint los canales válidos
  • Considera usar 'never' en tipos condicionales para casos no manejados
  • Prueba tu tipado intentando pasar combinaciones inválidas y verificando que TypeScript las rechace

Evalua tu comprension

Completa el quiz interactivo de arriba para ganar XP.