Conceptos avanzados sobre tipos genéricos
En esta sección se pretenden ilustrar algunos conceptos avanzados sobre tipos genéricos, en concreto, colecciones genéricas e iteradores genéricos.
Colecciones genéricas: Set y Map
TypeScript permite utilizar las colecciones proporcionadas por JavaScript con parámetros de tipo genéricos. Las colecciones proporcionadas por JavaScript son las siguientes:
Map<K, V>: Permite definir un mapa cuyas claves son de tipoKy sus valores de tipoV.ReadonlyMap<K, V>: Permite definir unMapque no puede modificarse.Set<T>: Permite definir un conjunto cuyos elementos son de tipoT.ReadonlySet<T>: Permite definir un conjunto no modificable.
De este modo, supongamos uno de nuestros ejemplos del capítulo anterior, en el que, en lugar de
modelar nuestra colección mediante un array, utilizamos alguna de las colecciones genéricas
anteriores como, por ejemplo, un conjunto Set:
type ColorType = 'red' | 'yellow' | 'blue' | 'green';
abstract class TwoDimensionalFigure {
constructor(private readonly name: string, private color: ColorType) {
}
getName() {
return this.name;
}
getColor() {
return this.color;
}
setColor(color: ColorType) {
this.color = color;
}
abstract getArea(): number;
abstract print(): void;
}
class Rectangle extends TwoDimensionalFigure {
private readonly sides = 4;
constructor(name: string, color: ColorType,
private base: number = 1, private height: number = 1) {
super(name, color);
}
getSides() {
return this.sides;
}
getArea() {
return this.base * this.height;
}
print() {
console.log(`I am a ${this.getName()}, I have ${this.getSides()} sides, ` +
`and my area is ${this.getArea()}`);
}
}
class FigureCollection<T extends TwoDimensionalFigure> {
private figures: Set<T>;
constructor(figures: T[]) {
this.figures = new Set(figures);
}
addFigure(newFigure: T) {
this.figures.add(newFigure);
}
getNumberOfFigures() {
return this.figures.size;
}
getFigure(name: string) {
return [...this.figures.values()].find((figure) =>
figure.getName() === name);
}
print() {
//this.figures.forEach((figure) => {
// figure.print();
//});
for (const figure of this.figures) {
figure.print();
}
}
}
const myTwoDimensionalFigureCollection = new FigureCollection<TwoDimensionalFigure>([
new Rectangle('RedRectangle', 'red', 10, 5),
new Rectangle('GreenRectangle', 'green', 5, 30),
]);
myTwoDimensionalFigureCollection.print();
Se puede observar como la clase FigureCollection ahora incluye, en lugar de un array
figures cuyo tipo es T[], un conjunto figures cuyo tipo es Set<T>. Además, también
puede observarse como se han modificado algunos de los métodos de la clase FigureCollection
con el objetivo de adaptarlos al uso de Set.
Por último, obsérvese el uso de un bucle for..of dentro del método print de FigureCollection.
Este bucle permite recorrer un objeto que es iterable como, por ejemplo, un array, un conjunto o un mapa.
En próximos apartados veremos qué se entiende por objeto iterable y cómo podemos implementar nuestros
propios objetos iterables.
A continuación, se ilustra el mismo ejemplo anterior, pero utilizando Map, en lugar de
Set:
type ColorType = 'red' | 'yellow' | 'blue' | 'green';
abstract class TwoDimensionalFigure {
constructor(private readonly name: string, private color: ColorType) {
}
getName() {
return this.name;
}
getColor() {
return this.color;
}
setColor(color: ColorType) {
this.color = color;
}
abstract getArea(): number;
abstract print(): void;
}
class Rectangle extends TwoDimensionalFigure {
private readonly sides = 4;
constructor(name: string, color: ColorType,
private base: number = 1, private height: number = 1) {
super(name, color);
}
getSides() {
return this.sides;
}
getArea() {
return this.base * this.height;
}
print() {
console.log(`I am a ${this.getName()}, I have ${this.getSides()} sides, ` +
`and my area is ${this.getArea()}`);
}
}
class FigureCollection<T extends TwoDimensionalFigure> {
private figures: Map<string, T>;
constructor(figures: T[]) {
this.figures = new Map<string, T>();
figures.forEach((figure) => this.addFigure(figure));
}
addFigure(newFigure: T) {
this.figures.set(newFigure.getName(), newFigure);
}
getNumberOfFigures() {
return this.figures.size;
}
getFigure(name: string) {
return this.figures.get(name);
}
print() {
// this.figures.forEach((figure) => figure.print());
for (const [figureName, figure] of this.figures) {
console.log(figureName);
figure.print();
}
}
}
const myTwoDimensionalFigureCollection = new FigureCollection<TwoDimensionalFigure>([
new Rectangle('RedRectangle', 'red', 10, 5),
new Rectangle('GreenRectangle', 'green', 5, 30),
]);
myTwoDimensionalFigureCollection.print();
En el ejemplo anterior, la clase FigureCollection ahora cuenta con un atributo figures
de tipo Map<string, T>, lo cual quiere decir que las claves serán de tipo string y los
valores de tipo T. Además, obsérvese como el método addFigure añade una nueva entrada
al atributo figures utilizando el método set que recibe un par clave-valor. En este
caso, las claves vienen dadas por la propiedad name de cada nueva figura añadida. También
nótese como se utiliza el método addFigure en el constructor de FigureCollection. Por
último, véase el uso del bucle for..of en el método print de FigureCollection.
Iteradores genéricos
Los iteradores permiten enumerar una secuencia de valores y es muy común que una clase que opera sobre otros tipos como, por ejemplo, colecciones, proporcione soporte para iteradores. TypeScript proporciona las siguientes interfaces que permiten describir iteradores y sus resultados:
Iterator<T>: Esta interfaz describe un iterador cuyo métodonextdevuelve objetosIteratorResult<T>.IteratorResult<T>: Esta interfaz describe el resultado producido por un iterador mediante las propiedadesdoneyvalue.Iterable<T>: Esta interfaz define un objeto que tiene una propiedadSymbol.iteratory que permite iteración directa.IterableIterator<T>: Esta interfaz combina la interfacesIterator<T>eIterable<T>para describir un objeto que tiene una propiedadSymbol.iteratory que define un métodonexty una propiedadresult.
A continuación, se muestra el uso de Iterator<T> e IteratorResult<T> para proporcionar
acceso al atributo figures de la clase FiguresCollection de nuestro último ejemplo (solo
se muestra a partir de la clase FigureCollection):
class FigureCollection<T extends TwoDimensionalFigure> {
private figures: Map<string, T>;
constructor(figures: T[]) {
this.figures = new Map<string, T>();
figures.forEach((figure) => this.addFigure(figure));
}
addFigure(newFigure: T) {
this.figures.set(newFigure.getName(), newFigure);
}
getNumberOfFigures() {
return this.figures.size;
}
getFigure(name: string) {
return this.figures.get(name);
}
getFigures(): Iterator<T> {
return this.figures.values();
}
}
const myTwoDimensionalFigureCollection =
new FigureCollection<TwoDimensionalFigure>([
new Rectangle('RedRectangle', 'red', 10, 5),
new Rectangle('GreenRectangle', 'green', 5, 30),
]);
const myIterator: Iterator<TwoDimensionalFigure> =
myTwoDimensionalFigureCollection.getFigures();
let result: IteratorResult<TwoDimensionalFigure> = myIterator.next();
while (!result.done) {
result.value.print();
result = myIterator.next();
}
Hemos añadido un método getFigures a la clase FigureCollection que devuelve un
Iterator<T> (aunque como veremos más tarde, en realidad, el método values del
atributo figures, cuyo tipo es Map<string, T>, devuelve un IterableIterator<T>).
Al invocar este método sobre el objeto myTwoDimensionalFigureCollection, se obtiene un
Iterator<TwoDimensionalFigure>, cuyo método next produce objetos
IteratorResult<TwoDimensionalFigure>. La propiedad value de cada objeto
IteratorResult<TwoDimensionalFigure> devolverá un objeto TwoDimensionalFigure,
permitiendo iterar sobre los objetos de la colección.
Cabe mencionar en este punto que los iteradores se introdujeron en la versión ES6 del
estándar de JavaScript, por lo que si se desea utilizar iteradores en un proyecto TypeScript
para el cual se está generando código JavaScript correspondiente a una versión anterior,
se debe habilitar la opción downlevelIteration del compilador de TypeScript.
Combinación de un Iterable y un Iterator
En este caso, se muestra el uso de IterableIterator<T>:
class FigureCollection<T extends TwoDimensionalFigure> {
private figures: Map<string, T>;
constructor(figures: T[]) {
this.figures = new Map<string, T>();
figures.forEach((figure) => this.addFigure(figure));
}
addFigure(newFigure: T) {
this.figures.set(newFigure.getName(), newFigure);
}
getNumberOfFigures() {
return this.figures.size;
}
getFigure(name: string) {
return this.figures.get(name);
}
getFigures(): IterableIterator<T> {
return this.figures.values();
}
}
const myTwoDimensionalFigureCollection =
new FigureCollection<TwoDimensionalFigure>([
new Rectangle('RedRectangle', 'red', 10, 5),
new Rectangle('GreenRectangle', 'green', 5, 30),
]);
for (const figure of myTwoDimensionalFigureCollection.getFigures()) {
figure.print();
}
La única diferencia respecto al ejemplo anterior es que ahora, en lugar de anotar el tipo devuelto por
getFigures de FigureCollection como Iterator<T>, se anota como IterableIterator<T>,
que es lo que realmente devuelve el método values de un Map.
De este modo, el IterableIterator devuelto por getFigures puede ser iterado directamente
haciendo uso de un bucle for..of.
Crear una clase iterable implementando la interfaz Iterable
Las clases que implementan la interfaz Iterable<T> deben definir una propiedad
Symbol.iterator, lo que permite iterar en los elementos de la colección sin
necesidad de invocar un método propio como getFigures:
class FigureCollection<T extends TwoDimensionalFigure> implements Iterable<T> {
private figures: Map<string, T>;
constructor(figures: T[]) {
this.figures = new Map<string, T>();
figures.forEach((figure) => this.addFigure(figure));
}
addFigure(newFigure: T) {
this.figures.set(newFigure.getName(), newFigure);
}
getNumberOfFigures() {
return this.figures.size;
}
getFigure(name: string) {
return this.figures.get(name);
}
[Symbol.iterator](): IterableIterator<T> {
return this.figures.values();
}
}
const myTwoDimensionalFigureCollection =
new FigureCollection<TwoDimensionalFigure>([
new Rectangle('RedRectangle', 'red', 10, 5),
new Rectangle('GreenRectangle', 'green', 5, 30),
]);
for (const figure of myTwoDimensionalFigureCollection) {
figure.print();
}
Se puede observar como ahora la clase FigureCollection implementa la interfaz Iterable<T>,
además de que el método getFigures ha sido eliminado y, ahora en su lugar, se ha definido la propiedad
Symbol.iterator, la cual devuelve un objeto IterableIterator<T> que puede utilizarse para iterar.
De hecho, el cuerpo del anterior método getFigures y de la nueva propiedad Symbol.iterator son
exactamente iguales. Lo anterior permite que ya no se tenga que invocar al método getFigures para
iterar sobre los elementos de myTwoDimensionalFigureCollection a través de un bucle for..of.