Menu

Genéricos en TypeScript: funciones, tipos y clases genéricas

Los genéricos permiten que una función, interfaz, tipo o clase funcione con muchos tipos manteniéndolos conectados: lo que entra decide lo que sale. Aprende las funciones genéricas, la inferencia de argumentos de tipo, varios parámetros de tipo, interfaces y clases genéricas, valores por defecto y cuándo no usar genéricos.

Esta página incluye editores ejecutables: edita, ejecuta y ve el resultado al instante.

Los genéricos son parámetros de tipo. Una función genérica declara un tipo marcador, normalmente llamado T, y cada llamada lo rellena, así que los tipos que entran deciden los tipos que salen. Una misma función funciona entonces con números, strings y tus propios objetos, y cada llamada sigue estando totalmente tipada.

<T> después del nombre de la función declara el parámetro de tipo. items: T[] lo usa para la entrada y T | undefined para la salida, lo que une las dos. Nunca escribiste number ni string: TypeScript infirió T a partir del argumento.

¿Por qué no any o unknown?

Sin genéricos tiparías el parámetro como any o unknown. Los dos aceptan cualquier array, y los dos pierden la conexión entre la entrada y la salida:

Tipo del parámetroAcepta cualquier entradaTipo del resultadoSe comprueba
anysíanyno: los errores compilan
unknownsíunknownsí, pero tienes que estrecharlo antes de usarlo
T (genérico)síligado a la entradasí, con el tipo preciso

Inferencia de los argumentos de tipo

Normalmente TypeScript infiere los argumentos de tipo a partir de los valores que pasas. También puedes escribirlos de forma explícita entre corchetes angulares en la llamada, lo que hace falta cuando nada en los argumentos menciona T, o cuando la inferencia elige algo que no querías.

Hay dos resultados de la inferencia que conviene conocer. pair(1, "one") sin el argumento explícito es un error: TypeScript infiere T = number a partir del primer argumento y luego informa Argument of type 'string' is not assignable to parameter of type 'number'. (TS2345). Y emptyList() sin ningún argumento obtiene T = unknown, lo que da unknown[], que rara vez es lo que quieres.

Varios parámetros de tipo

Una función puede declarar tantos parámetros de tipo como necesite, separados por comas. Cada uno se infiere por separado.

mapValues infiere tres cosas a la vez: K es "tea" | "cake", V es number y R es string por lo que devuelve el callback. K extends string es una restricción, que se explica en la página siguiente.

Interfaces y alias de tipo genéricos

Los tipos también pueden recibir parámetros. Entonces escribes el argumento al usar el tipo: Box<number>, ApiResponse<User>. La mayoría de los tipos de colección integrados funcionan así: Array<T>, Map<K, V>, Promise<T>, Record<K, V>.

Result<T, E = string> también muestra un parámetro de tipo por defecto: Result<number> significa Result<number, string>. Igual que los parámetros opcionales de una función, los parámetros con valor por defecto deben ir después de los obligatorios.

Clases genéricas

Una clase recibe parámetros de tipo después de su nombre, y cada instancia los fija. Los campos, los métodos y los parámetros del constructor pueden usarlos.

Si el constructor recibe un T, puedes omitir el argumento explícito: new Box(5) infiere Box<number>. Los miembros estáticos pertenecen a la propia clase, no a una instancia, así que no pueden usar el parámetro de tipo de la clase (Static members cannot reference class type parameters., TS2302).

Funciones flecha genéricas y tipos de función

La lista de parámetros de tipo va antes de la lista de parámetros. La misma sintaxis describe un tipo de función genérico.

const last = <T>(items: T[]): T | undefined => items[items.length - 1];

type Mapper = <T, R>(items: T[], fn: (item: T) => R) => R[];
const mapAll: Mapper = (items, fn) => items.map(fn);

// In a .tsx file, <T> looks like a JSX tag. Add a trailing comma:
const lastTsx = <T,>(items: T[]) => items[items.length - 1];

Inferir tipos literales con parámetros de tipo const

Por defecto un genérico infiere tipos ensanchados: pair("a", "b") da [string, string]. Cuando importan los valores exactos (nombres de rutas, nombres de eventos, listas de columnas), marca el parámetro como const (TypeScript 5.0 y posteriores) y se infiere como si el argumento estuviera escrito con as const:

Quien llama escribe un array normal, y la función conserva los tipos literales.

Cuándo no usar genéricos

Un parámetro de tipo se gana su sitio cuando conecta dos cosas: un parámetro y el tipo de retorno, dos parámetros, o un parámetro y un callback. Si T aparece una sola vez, no aporta nada y un tipo normal es más claro.

// Pointless: T is used once, so it is just a longer way to write unknown
function logValue<T>(value: T): void {
  console.log(value);
}

// Clearer
function logValueSimple(value: unknown): void {
  console.log(value);
}

// Also pointless: returns T but nothing connects T to an argument,
// so the caller is really just asserting a type
function parseJson<T>(text: string): T {
  return JSON.parse(text);
}

El último es habitual y engañoso: parseJson<User>(text) parece seguro en tipos pero no comprueba nada, exactamente igual que JSON.parse(text) as User. Devuelve unknown y valida. Otras señales de abuso: un parámetro de tipo al que siempre se le pasa el mismo argumento, o uno que se podría sustituir por una unión de dos tipos conocidos.

Para genéricos que necesitan saber algo de T (que tiene un length, o que K es una clave de T), el siguiente paso son las restricciones con extends.

Preguntas frecuentes

¿Qué son los genéricos en TypeScript?

Los genéricos son parámetros de tipo: marcadores como T que se rellenan con un tipo real cada vez que se usa una función, interfaz, alias de tipo o clase. function first<T>(items: T[]): T | undefined funciona con cualquier array, y el resultado tiene el tipo de los elementos del array que pasaste, así que first([1, 2]) es number | undefined y first(["a"]) es string | undefined.

¿Qué diferencia hay entre los genéricos y any?

any desactiva la comprobación de tipos: el valor que entra y el que sale no tienen relación, y el resultado también es any. Un genérico mantiene la relación: identity<T>(x: T): T devuelve exactamente el tipo que pasaste. Usa unknown si aceptas cualquier cosa pero no necesitas devolver el tipo, y un genérico si lo necesitas.

¿Qué significa <T> en TypeScript?

Declara un parámetro de tipo llamado T. En function wrap<T>(value: T), T es una variable de tipo que TypeScript rellena a partir del argumento en cada llamada, o que pasas de forma explícita como wrap<string>("a"). El nombre T es solo una convención; nombres más largos como TItem o Key funcionan igual.

¿Cómo escribo una función flecha genérica en TypeScript?

Pon la lista de parámetros de tipo antes de los parámetros: const first = <T>(items: T[]): T | undefined => items[0];. En un archivo .tsx, <T> se leería como una etiqueta JSX, así que escribe <T,> con una coma final, o <T extends unknown>.

¿Cómo pongo un tipo por defecto a un genérico en TypeScript?

Añade = Type después del parámetro: interface ApiResponse<T = unknown> { data: T }. Así ApiResponse sin argumento significa ApiResponse<unknown>. Los parámetros con valor por defecto deben ir después de los que no lo tienen, como los parámetros opcionales de una función.

Coddy programming languages illustration

Aprende a programar con Coddy

COMENZAR