Saltar al contenido

Lección 18 de 20 · Lectura y práctica

Implementar Módulos con Genéricos y Patrones

Concepto claveLos módulos con genéricos en TypeScript son una técnica avanzada que permite crear componentes reutilizables y tipados de forma segura en arquitecturas complejas. Imagina que estás construyendo una biblioteca de.

Del concepto a la práctica

Taller de esta lección

Un repositorio genérico puede centralizar operaciones de colección sin ocultar el tipo de entidad. La restricción exige una identidad común y permite recuperar el registro correcto.

Ejemplo resuelto

ts
type Entity = { id: string };
class Repository<T extends Entity> {
  private rows: T[];
  constructor(rows: readonly T[]) { this.rows = [...rows]; }
  find(id: string): T | undefined { return this.rows.find(r => r.id === id); }
  all(): T[] { return [...this.rows]; }
}
const repo = new Repository([{ id: "t1", title: "Leer" }]);
console.log(repo.find("t1")?.title); // Leer

Pruébalo

  1. Declara un repositorio de usuarios con id y name.

  2. Recupera u1.

  3. Comprueba que el resultado puede usar name tras verificarlo.

  4. Intenta construirlo con objetos sin id.

Comprueba tu resultado

La consulta válida debe mostrar Leer; la consulta inexistente debe ser undefined.

Error frecuente: devolver el arreglo interno directamente. Corrígelo con una copia en all(), evitando que añadir o quitar posiciones del array devuelto cambie el array almacenado.

La copia es superficial: los objetos de all y find siguen compartidos y cambiar su title altera el objeto guardado. Si necesitas aislamiento expón modelos readonly y devuelve copias de cada objeto, o diseña un método update que aplique reglas del dominio; objetos anidados necesitan otro contrato. Prueba además find("ausente") y confirma undefined. Copiar el array del constructor evita que push sobre la colección de entrada cambie la cantidad almacenada después.

Diagrama de decisión con ramas Sí y No y un bucle que procesa cada elemento de una lista.
CursaloMapa de práctica
Cómo avanza un programa

Una condición decide qué camino ejecuta el programa. Un bucle repite una operación mientras queden elementos, y termina cuando la lista se agota. Separar ambas ideas ayuda a seguir el flujo y comprobar casos límite.

Ver la infografía en tamaño completo ↗
Fuentes para profundizar

Concepto clave

Los módulos con genéricos en TypeScript son una técnica avanzada que permite crear componentes reutilizables y tipados de forma segura en arquitecturas complejas. Imagina que estás construyendo una biblioteca de componentes frontend donde cada componente debe manejar diferentes tipos de datos (como usuarios, productos o configuraciones), pero manteniendo una interfaz consistente. Los genéricos actúan como "placeholders" de tipos que se concretan en tiempo de uso, similar a cómo una plantilla de documento se adapta a diferentes contenidos sin cambiar su estructura.

Este enfoque combina la modularidad de los módulos ES6 con el poder del tipado estático de TypeScript. En lugar de crear múltiples versiones de un módulo para cada tipo de dato, defines una sola implementación parametrizada con tipos genéricos. Esto reduce la duplicación de código y mejora el mantenimiento, especialmente en proyectos grandes donde la consistencia de tipos es crítica para evitar errores en tiempo de ejecución.

Cómo funciona en la práctica

Vamos a implementar un módulo genérico paso a paso. Supongamos que necesitamos un store de estado reutilizable para diferentes entidades en una aplicación frontend.

// store.ts - Módulo genérico exportado
export interface Store {
  getState(): T;
  setState(newState: T): void;
  subscribe(listener: (state: T) => void): () => void;
}

export function createStore(initialState: T): Store {
  let state: T = initialState;
  const listeners: Array<(state: T) => void> = [];
  
  return {
    getState: () => state,
    setState: (newState: T) => {
      state = newState;
      listeners.forEach(listener => listener(state));
    },
    subscribe: (listener) => {
      listeners.push(listener);
      return () => {
        const index = listeners.indexOf(listener);
        if (index > -1) listeners.splice(index, 1);
      };
    }
  };
}

Este módulo exporta una interfaz Store<T> y una función createStore<T> que puede ser instanciada con cualquier tipo T. Al usarlo, TypeScript inferirá y validará los tipos automáticamente:

// Uso en otro archivo
import { createStore } from './store';

interface UserState {
  name: string;
  age: number;
}

const userStore = createStore({ name: 'Ana', age: 30 });
userStore.setState({ name: 'Carlos', age: 25 }); // Válido
// userStore.setState({ name: 'Luis' }); // Error: falta 'age'

Caso de estudio

En una arquitectura frontend para un e-commerce, implementamos un módulo genérico ApiClient que maneja peticiones HTTP para diferentes recursos. Este módulo usa genéricos para tipar las respuestas y parámetros, asegurando consistencia en toda la aplicación.

// apiClient.ts
export class ApiClient {
  constructor(private baseUrl: string) {}
  
  async getAll(): Promise {
    const response = await fetch(`${this.baseUrl}`);
    return response.json();
  }
  
  async create(data: TCreate): Promise {
    const response = await fetch(this.baseUrl, {
      method: 'POST',
      body: JSON.stringify(data)
    });
    return response.json();
  }
  
  async update(id: string, data: TUpdate): Promise {
    const response = await fetch(`${this.baseUrl}/${id}`, {
      method: 'PUT',
      body: JSON.stringify(data)
    });
    return response.json();
  }
}

// Uso para productos
interface Product {
  id: string;
  name: string;
  price: number;
}

interface CreateProductDto {
  name: string;
  price: number;
}

interface UpdateProductDto {
  price?: number;
}

const productClient = new ApiClient('/api/products');
// productClient.create({ name: 'Laptop' }); // Error: falta 'price'
// productClient.update('123', { price: 999 }); // Válido
Los genéricos con múltiples parámetros de tipo, como en ApiClient<TResource, TCreate, TUpdate>, permiten modelar operaciones complejas con precisión, evitando el uso de any y mejorando la autocompletación en IDEs.

Errores comunes

  • Usar any en lugar de genéricos: Esto elimina los beneficios del tipado estático. En lugar de function getItem(key: string): any, usa function getItem<T>(key: string): T para preservar la seguridad de tipos.
  • No restringir genéricos cuando es necesario: Si un genérico debe cumplir ciertas condiciones, usa extends. Por ejemplo, T extends { id: string } asegura que T tenga una propiedad id.
  • Ignorar la inferencia de tipos: TypeScript puede inferir genéricos en muchos casos. No siempre necesitas especificarlos explícitamente, lo que puede simplificar el código.
  • Crear genéricos demasiado complejos: Evita anidar múltiples genéricos sin necesidad, ya que puede hacer el código difícil de leer. Prioriza la simplicidad.
  • No documentar los genéricos: En módulos compartidos, documenta el propósito de cada parámetro genérico con comentarios JSDoc para mejorar la mantenibilidad.

Checklist de dominio

  1. Puedo implementar un módulo que exporte una clase o función con al menos un parámetro genérico.
  2. Sé usar extends para aplicar restricciones a genéricos, como T extends Serializable.
  3. He aplicado genéricos en un contexto real, como un store de estado o un cliente API.
  4. Puedo explicar la diferencia entre Array<T> y T[] (son equivalentes, pero el primero es más explícito con genéricos).
  5. Evito el uso de any en módulos genéricos, optando por tipos específicos o unknown con type guards.
  6. Comprendo cómo TypeScript infiere tipos genéricos a partir del uso y cuándo es necesario especificarlos manualmente.
  7. He revisado y refactorizado código existente para introducir genéricos donde haya duplicación de tipos.

Implementar un módulo genérico para formularios dinámicos

En este ejercicio, crearás un módulo TypeScript genérico que maneje formularios dinámicos en una aplicación frontend. Sigue estos pasos:

  1. Crea un archivo formManager.ts que exporte una clase FormManager<T>.
  2. Define la clase con estos métodos:
    • constructor(initialData: T): inicializa el estado del formulario.
    • getField<K extends keyof T>(field: K): T[K]: devuelve el valor de un campo específico.
    • setField<K extends keyof T>(field: K, value: T[K]): void: actualiza el valor de un campo.
    • validate(): boolean: valida que todos los campos cumplan reglas básicas (ej., no estar vacíos si son strings).
  3. Usa tipos genéricos para asegurar que getField y setField sean type-safe, evitando asignaciones incorrectas.
  4. Implementa un ejemplo de uso en otro archivo, definiendo una interfaz UserForm con campos como name: string, email: string, y age: number.
  5. Prueba el módulo creando una instancia de FormManager<UserForm> y llamando a sus métodos.
Pistas
  • Usa keyof T para restringir los parámetros de campo a las claves de T.
  • Considera agregar un método getData(): T para obtener todos los datos del formulario.
  • Para validación, puedes asumir que los campos string no deben estar vacíos y los números deben ser positivos.

Evalua tu comprension

Completa el quiz interactivo de arriba para ganar XP.

Sigue con TypeScript Avanzado: Patrones Genéricos y Tipado Profundo para Arquitecturas Complejas

Esta lección forma parte de un curso de Cursalo. Revisa su programa y elige entre la compra individual o el acceso a toda la escuela.

Si ya tenías acceso a este curso, entra con tu cuenta: tus derechos y tu progreso se conservan. No necesitas comprarlo de nuevo.

01 / ELEGIR UNA RUTA

El próximo paso

Aprende IA mientras construyes algo que sí necesitas.

Empieza por el curso inicial o explora el catálogo para encontrar una práctica alineada con tu trabajo.