Los genéricos proporcionan fragmentos de código reutilizables que funcionan con varios tipos en lugar de con un único tipo. Los genéricos permiten tratar el tipo como una variable y especificarlo al utilizarlo, de forma similar a los parámetros de función.
Los genéricos pueden utilizarse junto con funciones (creación de función genérica), clases (creación de clase genérica) e interfaces (creación de interfaz genérica).
Uso básico
Probablemente hayas utilizado genéricos en el pasado incluso sin saberlo - el uso más común de genéricos es declarar un array:
const miMatriz: cadena[];
No es demasiado especial a primera vista, sólo declaramos miMatriz
como una matriz de cadenas, pero es lo mismo que una declaración genérica:
const myArray: Array;
Mantener las cosas explícitas
Empecemos con un ejemplo muy sencillo: cómo podríamos portar esta función vanilla JS a TypeScript:
function getPrefiledArray(relleno, longitud) {
return (new Array(length)).fill(filler);
}
Esta función devolverá un array lleno con la cantidad dada de relleno
Así que longitud
será número
y la función completa devolverá un array de relleno
- pero, ¿qué es el relleno? En este punto puede ser cualquier cosa, así que una opción es utilizar cualquier
:
function getPrefiledArray(filler: any, length: number): any[] {
return (new Array(length)).fill(filler);
}
Utilizando cualquier
es ciertamente genérico - podemos pasar literalmente cualquier cosa, por lo que "trabajar con un número de tipos en lugar de un único tipo" de la definición está totalmente cubierto, pero perdemos la conexión entre relleno
y el tipo de retorno. En este caso, queremos devolver alguna cosa común, y podemos definir explícitamente esta cosa común como parámetro de tipo:
function getPrefiledArray(relleno: T, longitud: número): T[] {
return (new Array(length)).fill(filler);
}
y usarlo así:
const prefilledArray = getPrefiledArray(0, 10);
Restricciones genéricas
Veamos otros casos, probablemente más comunes. ¿Por qué usamos tipos en las funciones? Para mí, es para asegurar que los argumentos pasados a la función tendrán algunas propiedades con las que quiero interactuar.
Una vez más vamos a intentar portar una simple función vanilla JS a TS.
function getLength(cosa) {
return cosa.longitud;
}
Tenemos un enigma no trivial: ¿cómo garantizar que la cosa tiene un longitud
propiedad, y el primer pensamiento puede ser hacer algo como:
function getLength(cosa: typeof Array):number {
return cosa.longitud;
}
y dependiendo del contexto puede ser correcto, en general somos un poco genéricos - funcionará con arrays de múltiples tipos, pero ¿qué pasa si no sabemos realmente si la cosa debe ser siempre un array - tal vez la cosa es un campo de fútbol o una cáscara de plátano? En este caso, tenemos que recoger las propiedades comunes de esa cosa en una construcción que puede definir las propiedades de un objeto - una interfaz:
interfaz IThingWithLength {
longitud: número;
}
Podemos utilizar IThingWithLength
como tipo de la interfaz cosa
parámetro:
function getLength(cosa: ITCosaConLongitud):number {
return cosa.longitud;
}
francamente en este sencillo ejemplo, estará perfectamente bien, pero si queremos mantener este tipo genérico, y no enfrentarnos al problema del primer ejemplo podemos usar Restricciones genéricas:
function getLength(cosa: T):number {
return cosa.longitud;
}
y úsalo:
interfaz IBananaPeel {
grosor: número;
longitud: número;
}
const bananaPeel: IBananaPeel = {espesor: 0.2, longitud: 3.14};
getLength(bananaPeel);
Utilizando extiende
garantiza que T
contendrá propiedades definidas por IThingWithLength
.
Clases genéricas
Hasta este punto, estábamos trabajando con funciones genéricas, pero no es el único lugar donde brillan los genéricos, veamos cómo podemos incorporarlos a las clases.
En primer lugar vamos a tratar de almacenar racimo de plátanos en la cesta de plátanos:
clase Banana {
constructor(
public longitud: número,
public color: cadena,
public radiación ionizante: número
) {}
}
clase BananaBasket {
privado bananas: Banana[] = [];
add(banana: Banana): void {
this.bananas.push(banana);
}
}
const bananaBasket = new BananaBasket();
bananaBasket.add(nuevo Banana(3.14, 'rojo', 10e-7));
Ahora vamos a tratar de crear cesta de propósito general, a diferentes cosas con el mismo tipo:
clase Cesta {
cosas privadas: T[] = [];
add(cosa: T): void {
this.stuff.push(cosa);
}
}
const bananaBasket = nueva Cesta();
Y por último, supongamos que nuestra cesta es un contenedor de material radiactivo y sólo podemos almacenar materia que tenga Radiación ionizante
propiedad:
interfaz IRadioactive {
ionizingRadiation: número;
}
clase RadioactiveContainer {
privado stuff: T[] = [];
add(cosa: T): void {
this.stuff.push(cosa);
}
}
Interfaz genérica
Por último intentemos reunir todos nuestros conocimientos y construyamos un imperio radioactivo utilizando también Interfaces Genéricas:
// Definir atributos comunes para contenedores
interfaz IRadioactivo {
ionizingRadiation: número;
}
// Definir algo que es radioactivo
interface IBanana extends IRadioactive {
longitud: número;
color: cadena;
}
// Definir algo que no es radioactivo
interfaz IDog {
peso: número;
}
// Define una interfaz para un contenedor que sólo puede contener cosas radioactivas
interface IRadioactiveContainer {
add(cosa: T): void;
getRadioactiveness():número;
}
// Definir clase que implemente la interfaz del contenedor radioactivo
class ContenedorRadioactivo implements ContenedorRadioactivo {
privado stuff: T[] = [];
add(cosa: T): void {
this.stuff.push(cosa);
}
getRadioactividad(): number {
return this.stuff.reduce((a, b) => a + b.ionizingRadiation, 0)
}
}
// ¡ERROR! El tipo 'IDog' no satisface la restricción 'IRadioactive'
// Y es un poco brutal almacenar perros dentro de un contenedor radioactivo
const perrosContenedor = new ContenedorRadioactivo();
// ¡Todo bien fam!
const radioactiveContainer = new RadioactiveContainer();
// Recuerda clasificar tus residuos radioactivos - crea un contenedor separado sólo para plátanos
const bananasContainer = nuevo RadioactiveContainer();
¡Eso es todo, amigos!