Genericidad
Introducción
La genericidad permite escribir código que funcione en varios tipos, en lugar de solo en uno. Su objetivo es mejorar la reutilización, lo que ayuda a evitar duplicar un bloque de código para cada tipo que puede utilizarlo. Es un concepto que existe en la programación orientada a objetos desde hace mucho tiempo y que se ha implementado en TypeScript desde la primera versión del lenguaje.
Declaración básica
La declaración de un tipo genérico se realiza por convención con la letra T. Es posible cambiar esta letra, pero debe estar encapsulada entre corchetes angulares.
Sintaxis:
<T>
El arreglo es uno de los ejemplos más simples para ilustrar el uso de genéricos. El archivo de declaración básico de TypeScript (lib.d.ts) tiene una interfaz para escribir elementos contenidos en una matriz. Esta interfaz usa genericidad: Array<T>.
Ejemplo:
const salaries: Array<number> = [1700, 2250, 2000, 1850];
salaries.push(2125);
La declaración de un tipo genérico se realiza siempre después del nombre del elemento que lo porta. Una vez definido, el tipo genérico se puede utilizar para escribir lo que está contenido en el bloque de código del elemento que lleva la genericidad (por ejemplo, para una función, el tipo genérico se puede aplicar a sus parámetros, su retorno, sus variables…).
Ejemplo:
function add<T>(value: T) {
// Do something here
}
Cuando se utiliza un elemento que define un genérico, es necesario especificar entre paréntesis angulares qué tipo se utilizará en lugar del tipo genérico.
Ejemplo:
add<number>(1);
add<string>("Evelyn");
add<boolean>(true);
TypeScript valida la coherencia...
Clases e interfaces
Los genéricos se pueden utilizar con clases e interfaces. Una vez que se define un genérico en una interfaz, se puede usar para escribir:
-
una propiedad,
-
los parámetros del método,
-
los valores de retorno del método.
Ejemplo:
interface Entity<T> {
readonly id: T;
setId(id: T): void;
getId(): T;
}
Al declarar una variable con una interfaz que define un genérico, se debe especificar esto.
Ejemplo:
interface Entity<T> {
readonly id: T;
}
const entityWithNumberId: Entity<number> = {
id: 1
};
// Log: 1
console.log(entityWithNumberId.id);
const entityWithStringId: Entity<string> = {
id: "2a826410-de77-4938-b640-571cd943f3f5"
};
// Log: "2a826410-de77-4938-b640-571cd943f3f5"
console.log(entityWithStringId.id);
// Compilation Error TS2345: Argument of type 'true' is
// not assignable to parameter of type 'string'.
const entityWithWrongType: Entity<string> = {
id: true
};
Se puede utilizar un tipo genérico definido en una clase para escribir:
-
una propiedad...
Restricciones
Se puede especificar una restricción de tipo al declarar un tipo genérico. Para hacerlo, debe usar la palabra clave extends y luego definir la restricción de tipo que debe respetarse.
Sintaxis:
<T extends Type>
Cuando se utiliza un elemento que define un genérico con una restricción, será necesario utilizar un tipo que se ajuste a ella.
Ejemplo:
interface Payable {
salary: number
}
class Company {
sendPayments<T extends Payable>(toBePaid: T[]) {
toBePaid.forEach(p => {
console.log(`Pay salary: ${ p.salary }€`);
});
}
}
Se puede aplicar cualquier tipo de forma a una restricción. Por tanto, es posible utilizar:
-
tipos primitivos (number, string, boolean…),
-
interfaces,
-
clases,
-
alias de tipo (consulte el capítulo Sistema de tipos avanzados),
-
tipos singleton (consulte el capítulo Sistema de tipos avanzados).
Se pueden aplicar varias restricciones al mismo tipo genérico utilizando el operador de intersección & (consulte el capítulo Sistema de tipos avanzados).
Ejemplo:
interface Payable {
salary: number;
}
interface Person {
firstName: string;
lastName: string;
}
class Company {
sendPayments<T extends Payable & Person>(employees: T[]) {
employees.forEach(e => {
console.log(
`Pay ${e.firstName} ${e.lastName} | Salary: ${e.salary}€
);
});
}
}
Cuando se definen varios tipos genéricos en un elemento, se pueden...
Genérico y constructor
Al definir un tipo genérico en un elemento, no es posible crear posteriormente instancias de este. Ello limita gravemente algunos escenarios comunes en la programación orientada a objetos. Varios lenguajes permiten definir la presencia de un constructor a nivel de restricción. Actualmente, esto no es posible en TypeScript.
Para contrarrestar esta limitación, existe una sintaxis especial para definir la firma de un constructor. Este constructor se puede utilizar para devolver instancias de tipos genéricos.
Sintaxis:
ctor: new () => T
Esta sintaxis permite pasar el constructor como parámetro (o asignarlo a una variable) y luego usarlo con la nueva palabra clave para crear instancias.
Ejemplo:
function create<T>(ctor: new () => T) {
return new ctor();
}
const emptyString = create(String);
// Log: true
console.log(emptyString instanceof String);
Esta sintaxis también permite definir varios parámetros esperados para un constructor.
Ejemplo:
class Person {
constructor(
public readonly firstName: string,
public readonly lastName: string
) { }
}
function createPerson<T extends Person>(
ctor: {...