TypeScript: Genéricos y código reutilizable

TypeScript: Genéricos y código reutilizable

Tabla de Contenido

Note

Séptimo artículo de la serie sobre TypeScript. Exploraremos los genéricos: funciones y clases que operan sobre tipos dinámicos sin renunciar a la verificación estática.

Los genéricos en TypeScript resuelven una tensión recurrente: escribir código reutilizable que funcione con cualquier tipo, pero sin caer en any y perder toda la seguridad. Un genérico introduce un parámetro de tipo —una variable que representa un tipo aún por determinar— que se resuelve en el momento de uso. Analicemos cómo aplicarlos.

Funciones genéricas

Una función genérica declara su parámetro de tipo con la sintaxis <T> antes de la lista de parámetros. T representa el tipo que recibirá, y la función puede usarlo para tipar argumentos y retorno:

// Función tradicional
export function genericFunction<T>(argumento: T): T {
  return argumento
}

// Arrow function
export const genericArrowFunction = <T>(argumento: T): T => argumento

La función anterior retorna exactamente el mismo tipo que recibe. Si la invocas con un string, TypeScript sabe que el retorno es string; si la invocas con un number, el retorno es number. Esto es lo que distingue a un genérico de any: la relación entre entrada y salida se preserva.

Info

T es el nombre convencional, pero es arbitrario. Para múltiples parámetros suelen usarse T, U, K, V, o nombres descriptivos como TItem cuando aportan claridad.

Por qué no basta con any

Comparemos las dos aproximaciones para entender el valor de los genéricos:

// Con any: se pierde la información de tipo
function identityAny(arg: any): any {
  return arg
}
const a = identityAny('hola')   // a es any → sin autocompletado ni verificación

// Con genéricos: el tipo se conserva
function identity<T>(arg: T): T {
  return arg
}
const b = identity('hola')      // b es string → seguridad completa

Con any, el resultado pierde todo su tipo y el typechecker deja de ayudar. Con un genérico, la firma encadena el tipo de entrada con el de salida.

Clases genéricas

Las clases también admiten parámetros de tipo, lo que permite construir estructuras de datos reutilizables y type-safe. El parámetro se declara junto al nombre de la clase y queda disponible en todos sus miembros:

class MyMap<K, V> {
  constructor(initialKey: K, initialValue: V) {
    // ...
  }
  get(key: K): V {
    // ...
  }
  set(key: K, value: V): void {
    // ...
  }
  static of<K, V>(k: K, v: V): MyMap<K, V> {
    // ...
  }
}

Al instanciarla, los tipos pueden indicarse explícitamente o inferirse de los argumentos:

let a = new MyMap<string, number>('k', 1)  // MyMap<string, number>
let b = new MyMap('k', true)               // MyMap<string, boolean> (inferido)

a.get('k')
b.set('k', false)
flowchart LR
    G["MyMap<K, V>"] --> A["MyMap<string, number>"]
    G --> B["MyMap<string, boolean>"]
    G --> C["MyMap<number, Hero>"]

Una sola definición genérica genera tantas variantes type-safe como combinaciones de tipos uses.

Restringir genéricos con extends

A veces un genérico no debe aceptar cualquier tipo, sino solo aquellos que cumplan cierta forma. La cláusula extends impone esa restricción y, a la vez, habilita el acceso seguro a los miembros garantizados:

function getLength<T extends { length: number }>(item: T): number {
  return item.length
}

getLength('hola')        // OK: string tiene length
getLength([1, 2, 3])     // OK: array tiene length
getLength(42)            // Error: number no tiene 'length'

T extends { length: number } restringe T a tipos que posean una propiedad length, permitiendo usarla con seguridad dentro de la función.

Conclusión

Los genéricos son la herramienta que permite escribir código verdaderamente reutilizable sin sacrificar la verificación de tipos. Las funciones genéricas encadenan el tipo de entrada con el de salida, las clases genéricas habilitan estructuras de datos type-safe, y las restricciones con extends acotan los tipos aceptados mientras exponen sus miembros de forma segura.

En el próximo artículo abordaremos los tipos avanzados: subtipos, supertipos, varianza y la jerarquía completa del sistema de tipos.

Etiquetas :