Desarrollo de Sistemas Informáticos - Grado en Ingeniería Informática - ULL

View project on GitHub

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 tipo K y sus valores de tipo V.
  • ReadonlyMap<K, V>: Permite definir un Map que no puede modificarse.
  • Set<T>: Permite definir un conjunto cuyos elementos son de tipo T.
  • 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étodo next devuelve objetos IteratorResult<T>.
  • IteratorResult<T>: Esta interfaz describe el resultado producido por un iterador mediante las propiedades done y value.
  • Iterable<T>: Esta interfaz define un objeto que tiene una propiedad Symbol.iterator y que permite iteración directa.
  • IterableIterator<T>: Esta interfaz combina la interfaces Iterator<T> e Iterable<T> para describir un objeto que tiene una propiedad Symbol.iterator y que define un método next y una propiedad result.

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.