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

View project on GitHub

Espacios de nombres y módulos ES

Existen dos maneras en las que podemos dividir nuestro código fuente en múltiples ficheros. La primera de ellas es el uso de espacios de nombres y la segunda es el uso de módulos ES. En este apartado ilustraremos el uso de ambos mecanismos, aunque cabe mencionar que los espacios de nombres no suelen utilizarse.

Espacios de nombres

TypeScript permite hacer uso de espacios de nombres. Un espacio de nombres es un formato de módulos específico que el lenguaje proporciona desde antes de la aparición de los módulos ES. En la actualidad, los espacios de nombres no se encuentran en desuso, pero también es cierto que la mayoría de características que proporcionan también son proporcionadas por los módulos ES. Es por ello que se recomienda el uso de módulos ES, en lugar de espacios de nombres, con el objetivo de que TypeScript y JavaScript se encuentren alineados en lo que a la gestión de módulos se refiere.

Para ilustrar su funcionamiento, supongamos el siguiente ejemplo de código fuente TypeScript que hemos utilizado anteriormente, el cual está incluido en el fichero index.ts dentro del directorio src de nuestro proyecto:

type ColorType = 'red' | 'yellow' | 'blue' | 'orange' | 'green';

abstract class ThreeDimensionalFigure {
  constructor(private readonly name: string, private color: ColorType) {
  }

  getName() {
    return this.name;
  }
  getColor() {
    return this.color;
  }
  setColor(color: ColorType) {
    this.color = color;
  }

  abstract getVolume(): number;
  abstract print(): void;
}

class Cube extends ThreeDimensionalFigure {
  private readonly faces = 6;

  constructor(name: string, color: ColorType, private base: number = 1,
    private height: number = 1, private depth: number = 1) {
    super(name, color);
  }

  getFaces() {
    return this.faces;
  }

  getVolume() {
    return this.base * this.height * this.depth;
  }

  print() {
    console.log(`I am a ${this.getName()}, I have ${this.getFaces()} faces, ` +
      `and my volume is ${this.getVolume()}`);
  }
}

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 | ThreeDimensionalFigure> {
  constructor(private figures: T[]) {
  }

  addFigure(newFigure: T) {
    this.figures.push(newFigure);
  }

  getNumberOfFigures() {
    return this.figures.length;
  }

  getFigure(index: number) {
    return this.figures[index];
  }

  print() {
    this.figures.forEach((figure) => {
      figure.print();
    });
  }
}

const myTwoDimensionalFigureCollection = new FigureCollection<TwoDimensionalFigure>([
  new Rectangle('RedRectangle', 'red', 10, 5),
  new Rectangle('GreenRectangle', 'green', 5, 30),
]);

const myThreeDimensionalFigureCollection = new FigureCollection<ThreeDimensionalFigure>([
  new Cube('RedCube', 'red', 10, 5, 4),
  new Cube('GreenCube', 'green', 5, 30, 7),
]);

myTwoDimensionalFigureCollection.print();
myThreeDimensionalFigureCollection.print();

También supongamos que las opciones habilitadas para el compilador de TypeScript son las siguientes:

{
  "exclude": [
    "./tests",
    "./node_modules",
    "./dist"
  ],
  "compilerOptions": {
    "target": "ES2024",
    "module": "CommonJS",
    "rootDir": "./src",
    "declaration": true,
    "outDir": "./dist",
    "strict": true,
  }
}

Vamos a dividir el código fuente del ejemplo anterior en múltiples ficheros utilizando, para ello, un espacio de nombres. Lo primero que haremos es crear un nuevo fichero denominado types.ts en el directorio src, el cual contendrá el siguiente fragmento de código que hemos extraído del fichero index.ts:

namespace App {
  type ColorType = 'red' | 'yellow' | 'blue' | 'orange' | 'green';
}

Tal y como puede observarse, dicho fragmento de código ha sido incluido en un espacio de nombres denominado App. Al intentar compilar el código fuente, el compilador de TypeScript emite numerosos errores relacionados con el tipo de datos ColorType:

src/index.ts:2:61 - error TS2304: Cannot find name 'ColorType'.

2   constructor(private readonly name: string, private color: ColorType) {
                                                              ~~~~~~~~~

src/index.ts:11:19 - error TS2304: Cannot find name 'ColorType'.

11   setColor(color: ColorType) {
                     ~~~~~~~~~

src/index.ts:22:36 - error TS2304: Cannot find name 'ColorType'.

22   constructor(name: string, color: ColorType, private base: number = 1,
                                      ~~~~~~~~~

src/index.ts:42:61 - error TS2304: Cannot find name 'ColorType'.

42   constructor(private readonly name: string, private color: ColorType) {
                                                               ~~~~~~~~~

src/index.ts:51:19 - error TS2304: Cannot find name 'ColorType'.

51   setColor(color: ColorType) {
                     ~~~~~~~~~

src/index.ts:62:36 - error TS2304: Cannot find name 'ColorType'.

62   constructor(name: string, color: ColorType,
                                      ~~~~~~~~~


Found 6 errors in the same file, starting at: src/index.ts:2

El tipo de datos propio ColorType ha pasado a formar parte del espacio de nombres App, que es diferente al espacio de nombres global, por defecto, donde se encuentra alojado el resto de nuestro código fuente.

También podría darse el caso de que al ejecutar el linter, obtengamos errores como los siguientes:

$eslint .

/home/usuario/DSI/theory-examples/src/types.ts
  1:1   error  ES2015 module syntax is preferred over namespaces  @typescript-eslint/no-namespace
  1:11  error  'App' is defined but never used                    @typescript-eslint/no-unused-vars
  2:8   error  'ColorType' is defined but never used              @typescript-eslint/no-unused-vars

✖ 3 problems (3 errors, 0 warnings)

Se puede observar como el linter, teniendo en cuenta el primer error, indica que es preferible utilizar módulos ES a utilizar espacios de nombres. Si le molesta lo anterior, recuerde que puede desactivar la regla correspondiente (@typescript-eslint/no-namespace) en el fichero de configuración eslint.config.mjs.

A continuación, observe como también ha cambiado el contenido del directorio dist:

[~/theory-examples(main)]$ls -lrtha dist/
total 20K
drwxrwxr-x 6 usuario usuario 4,0K abr  6 07:59 ..
-rw-rw-r-- 1 usuario usuario 2,4K abr  6 07:59 index.js
-rw-rw-r-- 1 usuario usuario   14 abr  6 07:59 types.js
drwxrwxr-x 2 usuario usuario 4,0K abr  6 07:59 .
-rw-rw-r-- 1 usuario usuario   28 abr  6 07:59 types.d.ts

Ahora, tal y como puede observarse, hemos pasado a tener dos ficheros con código fuente JavaScript. Es curioso el contenido que se ha generado en el fichero types.js alojado en dist:

"use strict";

No se ha generado código JavaScript porque, realmente, la definición de un tipo de datos propio es una característica de TypeScript y no de JavaScript.

El código fuente del fichero types.ts se encuentra disponible dentro del espacio de nombres App. Si, por ejemplo, quisiéramos utilizar el tipo de datos ColorType en otro fichero diferente al de su definición, deberemos exportarlo:

namespace App {
  export type ColorType = 'red' | 'yellow' | 'blue' | 'orange' | 'green';
}

A continuación, tenemos dos opciones para hacer uso de ColorType en el código del fichero index.ts. La primera consiste en preceder a todas las ocurrencias de ColorType con el nombre del espacio de nombres App seguido de un punto, además de importar de algún modo el código del fichero types.ts, para que pueda ser utilizado desde el fichero index.ts:

/// <reference path="types.ts"/>

abstract class ThreeDimensionalFigure {
  constructor(private readonly name: string, private color: App.ColorType) {
  }

  getName() {
    return this.name;
  }
  getColor() {
    return this.color;
  }
  setColor(color: App.ColorType) {
    this.color = color;
  }

  abstract getVolume(): number;
  abstract print(): void;
}

// Code goes on here

La segunda opción consistiría en introducir todo el código fuente de index.ts en el mismo espacio de nombres App:

/// <reference path="types.ts"/>

namespace App {
  abstract class ThreeDimensionalFigure {
    constructor(private readonly name: string, private color: ColorType) {
    }

    getName() {
      return this.name;
    }
    getColor() {
      return this.color;
    }
    setColor(color: ColorType) {
      this.color = color;
    }

    abstract getVolume(): number;
    abstract print(): void;
  }

// Code goes on here

Gracias a los anteriores cambios, ahora, el código compila y ejecuta correctamente, mostrando lo siguiente por la consola:

I am a RedRectangle, I have 4 sides, and my area is 50
I am a GreenRectangle, I have 4 sides, and my area is 150
I am a RedCube, I have 6 faces, and my volume is 200
I am a GreenCube, I have 6 faces, and my volume is 1050

El hecho de que el programa se haya ejecutado correctamente ha sido pura casualidad. En realidad, es importante recordar que todo el código JavaScript necesario para que nuestro programa funcione se ha generado en el fichero index.js (el fichero types.js se encuentra vacío).

De hecho, podríamos eliminar la siguiente línea del fichero index.ts y el programa seguiría ejecutándose correctamente:

/// <reference path="types.ts"/>

Cabe hacer un pequeño inciso en este punto, para indicar que si utiliza directivas como la anterior, al ejecutar el linter obtendrá un error indicándole que es preferible utilizar un estilo basado en sentencias import, en lugar de hacer uso de dichas directivas. En este caso, la regla que emite este tipo de errores es @typescript-eslint/triple-slash-reference.

Este tipo de directiva del compilador de TypeScript permite establecer dependencias entre ficheros en tiempo de compilación, además de establecer un orden en el que el compilador procesa los diferentes ficheros. Podemos eliminar la directiva porque, realmente, todo el código fuente se encuentra alojado en el directorio src, algo que hemos indicado a través de la opción rootDir del fichero de configuración del compilador de TypeScript. Además, el orden en el que se procesan los ficheros, en este caso, es el adecuado, aunque esto último podría no ser así dependiendo del caso particular al que nos enfrentemos. Por último, el compilador de TypeScript sabe lo que se incluye y no se incluye en un espacio de nombres concreto. Es por ello que no se produce ningún error en tiempo de compilación. En cualquier caso, el uso de directivas como la anterior solo se recomienda en aquellos casos en los que sea estrictamente necesario, es decir, cuando se obtengan errores por parte del compilador de TypeScript a la hora de no ser capaz de resolver una referencia concreta o de que no esté procesando los ficheros involucrados en la compilación en el orden correcto.

Gracias a los espacios de nombres, ahora podremos dividir nuestro código fuente en múltiples ficheros a lo largo de nuestro proyecto. Por ejemplo, creemos algunos ficheros más para incluir las clases abstractas, las clases concretas y las colecciones.

El contenido del fichero abstracts.ts almacenado en el directorio src será el siguiente:

namespace App {
  export abstract class ThreeDimensionalFigure {
    constructor(private readonly name: string, private color: ColorType) {
    }

    getName() {
      return this.name;
    }
    getColor() {
      return this.color;
    }
    setColor(color: ColorType) {
      this.color = color;
    }

    abstract getVolume(): number;
    abstract print(): void;
  }

  export 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;
  }
}

El contenido del fichero figures.ts almacenado en el directorio src será el siguiente:

namespace App {
  export class Cube extends ThreeDimensionalFigure {
    private readonly faces = 6;

    constructor(name: string, color: ColorType, private base: number = 1,
    private height: number = 1, private depth: number = 1) {
      super(name, color);
    }

    getFaces() {
      return this.faces;
    }

    getVolume() {
      return this.base * this.height * this.depth;
    }

    print() {
      console.log(`I am a ${this.getName()}, I have ${this.getFaces()} faces, ` +
      `and my volume is ${this.getVolume()}`);
    }
  }

  export 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()}`);
    }
  }
}

El contenido del fichero collections.ts, también almacenado en el directorio src, será tal y como se muestra a continuación:

namespace App {
  export class FigureCollection<T extends TwoDimensionalFigure | ThreeDimensionalFigure> {
    constructor(private figures: T[]) {
    }

    addFigure(newFigure: T) {
      this.figures.push(newFigure);
    }

    getNumberOfFigures() {
      return this.figures.length;
    }

    getFigure(index: number) {
      return this.figures[index];
    }

    print() {
      this.figures.forEach((figure) => {
        figure.print();
      });
    }
  }
}

Por último, el contenido del fichero index.ts será tal y como sigue:

namespace App {
  const myTwoDimensionalFigureCollection = new FigureCollection<TwoDimensionalFigure>([
    new Rectangle('RedRectangle', 'red', 10, 5),
    new Rectangle('GreenRectangle', 'green', 5, 30),
  ]);

  const myThreeDimensionalFigureCollection = new FigureCollection<ThreeDimensionalFigure>([
    new Cube('RedCube', 'red', 10, 5, 4),
    new Cube('GreenCube', 'green', 5, 30, 7),
  ]);

  myTwoDimensionalFigureCollection.print();
  myThreeDimensionalFigureCollection.print();
}

Tras haber llevado a cambio todos los cambios anteriores, el contenido de nuestro directorio src debería ser algo como lo siguiente:

[~/theory-examples(main)]$ls -lrtha src
total 28K
drwxrwxr-x 6 usuario usuario 4,0K abr  6 07:59 ..
drwxrwxr-x 2 usuario usuario 4,0K abr  6 08:50 .
-rw-rw-r-- 1 usuario usuario  729 abr  6 08:58 abstracts.ts
-rw-rw-r-- 1 usuario usuario  468 abr  6 08:58 collections.ts
-rw-rw-r-- 1 usuario usuario 1,1K abr  6 08:59 figures.ts
-rw-rw-r-- 1 usuario usuario   92 abr  6 09:06 types.ts
-rw-rw-r-- 1 usuario usuario  492 abr  6 09:11 index.ts

Al compilar el código fuente, el compilador de TypeScript no emite ningún error. Además, el contenido del directorio dist debería ser algo como lo siguiente:

[~/theory-examples(main)]$ls -lrtha dist/
total 48K
drwxrwxr-x 6 usuario usuario 4,0K abr  6 09:13 ..
-rw-rw-r-- 1 usuario usuario  848 abr  6 09:13 abstracts.js
-rw-rw-r-- 1 usuario usuario  676 abr  6 09:13 abstracts.d.ts
-rw-rw-r-- 1 usuario usuario  585 abr  6 09:13 collections.js
-rw-rw-r-- 1 usuario usuario  315 abr  6 09:13 collections.d.ts
-rw-rw-r-- 1 usuario usuario 1,3K abr  6 09:13 figures.js
-rw-rw-r-- 1 usuario usuario  660 abr  6 09:13 figures.d.ts
-rw-rw-r-- 1 usuario usuario   14 abr  6 09:13 types.js
-rw-rw-r-- 1 usuario usuario  542 abr  6 09:13 index.js
-rw-rw-r-- 1 usuario usuario   26 abr  6 09:13 index.d.ts
-rw-rw-r-- 1 usuario usuario   95 abr  6 09:13 types.d.ts
drwxrwxr-x 2 usuario usuario 4,0K abr  6 09:13 .

No obstante, veamos si nuestro programa ejecuta correctamente:

$node dist/index.js 
/home/usuario/DSI/theory-examples/dist/index.js:5
        new App.Rectangle('RedRectangle', 'red', 10, 5),
        ^

TypeError: App.Rectangle is not a constructor
    at /home/usuario/DSI/theory-examples/dist/index.js:5:9
    at Object.<anonymous> (/home/usuario/DSI/theory-examples/dist/index.js:14:3)
    at Module._compile (node:internal/modules/cjs/loader:1734:14)
    at Object..js (node:internal/modules/cjs/loader:1899:10)
    at Module.load (node:internal/modules/cjs/loader:1469:32)
    at Function._load (node:internal/modules/cjs/loader:1286:12)
    at TracingChannel.traceSync (node:diagnostics_channel:322:14)
    at wrapModuleLoad (node:internal/modules/cjs/loader:235:24)
    at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:151:5)
    at node:internal/main/run_main_module:33:47

Node.js v23.9.0

Se puede observar como se produce un error en tiempo de ejecución. ¿Por qué sucede lo anterior? Los espacios de nombres son una funcionalidad exclusiva de TypeScript y, desde el punto de vista de la compilación, no existe ningún problema porque el compilador de TypeScript es capaz de resolver todas las referencias necesarias.

En JavaScript los espacios de nombres no existen y el código JavaScript generado se encuentra disponible en múltiples ficheros independientes. El error anterior se produce cuando se intenta instanciar un objeto de la clase Rectangle. JavaScript no es capaz de encontrar la clase Rectangle ni tampoco su constructor en el fichero index.js que es el que se ha ejecutado.

Para resolver lo anterior, debemos modificar la opción outFile del compilador de TypeScript, para que tome el valor ./dist/app.js, de modo que todo el código fuente JavaScript generado se encuentre en un único fichero, y no en varios.

De este modo, si elminamos todo el directorio dist y volvemos a compilar el código fuente TypeScript, el contenido del dicho directorio será el siguiente:

[~/theory-examples(main)]$ls -lrtha dist/
total 16K
drwxrwxr-x 6 usuario usuario 4,0K abr  6 09:29 ..
-rw-rw-r-- 1 usuario usuario 3,2K abr  6 09:29 app.js
-rw-rw-r-- 1 usuario usuario 1,8K abr  6 09:29 app.d.ts
drwxrwxr-x 2 usuario usuario 4,0K abr  6 09:29 .

Con las últimas versiones, el compilador de TypeScript podría emitir los siguientes errores:

tsconfig.json:33:5 - error TS6082: Only 'amd' and 'system' modules are supported alongside --outFile.

33     "module": "CommonJS",                                /* Specify what module code is generated. */
       ~~~~~~~~

tsconfig.json:65:5 - error TS6082: Only 'amd' and 'system' modules are supported alongside --outFile.

65     "outFile": "./dist/app.js",                          /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
       ~~~~~~~~~


Found 2 errors in the same file, starting at: tsconfig.json:33

Esto se debe a que, en versiones recientes, el compilador de TypeScript no permite utilizar la opción outFile, a no ser que tengamos AMD o System como valor en la opción module del fichero de configuración. Esto es un problema porque nuestro código se ejecuta sobre Node.js, por lo que hemos estado utilizando el valor CommonJS para la opción module. Lo anterior hace que el uso de espacios de nombres tampoco sea adecuado cuando nuestro código JavaScript generado a partir de código TypeScript va a ejecutarse sobre Node.js.

A pesar de los errores anteriores, si solo utilizamos en nuestro código fuente espacios de nombres (y no una mezcla de espacios de nombres con módulos ES, algo que tampoco se recomienda), tras el proceso de compilación, el fichero app.js contendrá todo el código fuente JavaScript resultante del proceso de compilación de todos los ficheros con código fuente TypeScript. Es por ello que podremos ejecutar nuestro programa de la siguiente manera:

$node dist/app.js 
I am a RedRectangle, I have 4 sides, and my area is 50
I am a GreenRectangle, I have 4 sides, and my area is 150
I am a RedCube, I have 6 faces, and my volume is 200
I am a GreenCube, I have 6 faces, and my volume is 1050

Espacios de nombres y múltiples directorios

Supongamos que queremos dividir nuestro código fuente aún más y, además, organizarlo en múltiples directorios ubicados en el directorio src.

Partamos de la configuración del compilador utilizada en el apartado anterior. Además, se pueden eliminar todos los ficheros *.ts del directorio src, a excepción del fichero index.ts.

A continuación, vamos a crear los subdirectorios abstracts, collections, figures y types dentro del directorio src de nuestro proyecto:

[~/theory-examples(main)]$ls -l src/
total 20
drwxrwxr-x 2 usuario usuario 4096 abr  6 16:57 abstracts
drwxrwxr-x 2 usuario usuario 4096 abr  6 16:57 collections
drwxrwxr-x 2 usuario usuario 4096 abr  6 16:57 figures
-rw-rw-r-- 1 usuario usuario  786 abr  6 16:57 index.ts
drwxrwxr-x 2 usuario usuario 4096 abr  6 16:57 types

Dentro del directorio abstracts creamos los ficheros TwoDimensionalFigure.ts y ThreeDimensionalFigure.ts. El contenido del primer fichero será el siguiente:

namespace App {
  export 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;
  }
}

El contenido del fichero ThreeDimensionalFigure.ts será el siguiente:

namespace App {
  export abstract class ThreeDimensionalFigure {
    constructor(private readonly name: string, private color: ColorType) {
    }

    getName() {
      return this.name;
    }
    getColor() {
      return this.color;
    }
    setColor(color: ColorType) {
      this.color = color;
    }

    abstract getVolume(): number;
    abstract print(): void;
  }
}

A continuación, en el directorio collections, creamos un fichero FigureCollection.ts, al cual añadimos el siguiente código fuente:

namespace App {
  export class FigureCollection<T extends TwoDimensionalFigure | ThreeDimensionalFigure> {
    constructor(private figures: T[]) {
    }

    addFigure(newFigure: T) {
      this.figures.push(newFigure);
    }

    getNumberOfFigures() {
      return this.figures.length;
    }

    getFigure(index: number) {
      return this.figures[index];
    }

    print() {
      this.figures.forEach((figure) => {
        figure.print();
      });
    }
  }
}

Seguidamente, en el directorio figures, creamos los ficheros Rectangle.ts y Cube.ts. El contenido de Rectangle.ts será tal y como sigue:

namespace App {
  export 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()}`);
    }
  }
}

En el caso del fichero Cube.ts, el contenido será el siguiente:

namespace App {
  export class Cube extends ThreeDimensionalFigure {
    private readonly faces = 6;

    constructor(name: string, color: ColorType, private base: number = 1,
    private height: number = 1, private depth: number = 1) {
      super(name, color);
    }

    getFaces() {
      return this.faces;
    }

    getVolume() {
      return this.base * this.height * this.depth;
    }

    print() {
      console.log(`I am a ${this.getName()}, I have ${this.getFaces()} faces, ` +
      `and my volume is ${this.getVolume()}`);
    }
  }
}

En el directorio types, existirá un único fichero denominado ColorType.ts:

namespace App {
  export type ColorType = 'red' | 'yellow' | 'blue' | 'orange' | 'green';
}

Finalmente, el contenido del fichero index.ts ubicado en el directorio src ya debía contener lo siguiente:

namespace App {
  const myTwoDimensionalFigureCollection =
    new FigureCollection<TwoDimensionalFigure>([
      new Rectangle('RedRectangle', 'red', 10, 5),
      new Rectangle('GreenRectangle', 'green', 5, 30),
    ]);

  const myThreeDimensionalFigureCollection =
    new FigureCollection<ThreeDimensionalFigure>([
      new Cube('RedCube', 'red', 10, 5, 4),
      new Cube('GreenCube', 'green', 5, 30, 7),
    ]);

  myTwoDimensionalFigureCollection.print();
  myThreeDimensionalFigureCollection.print();
}

Tras compilar el ejemplo anterior, al ejecutar se produce el siguiente error:

$node dist/app.js 
/home/usuario/DSI/theory-examples/dist/app.js:5
        new App.Rectangle('RedRectangle', 'red', 10, 5),
        ^

TypeError: App.Rectangle is not a constructor
    at /home/usuario/DSI/theory-examples/dist/app.js:5:9
    at Object.<anonymous> (/home/usuario/DSI/theory-examples/dist/app.js:14:3)
    at Module._compile (node:internal/modules/cjs/loader:1734:14)
    at Object..js (node:internal/modules/cjs/loader:1899:10)
    at Module.load (node:internal/modules/cjs/loader:1469:32)
    at Function._load (node:internal/modules/cjs/loader:1286:12)
    at TracingChannel.traceSync (node:diagnostics_channel:322:14)
    at wrapModuleLoad (node:internal/modules/cjs/loader:235:24)
    at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:151:5)
    at node:internal/main/run_main_module:33:47

Node.js v23.9.0

El error se debe a que no se ha procesado el fichero Rectangle.ts. Para solucionar lo anterior, debemos incluir varias directivas reference-path al principio del fichero index.ts:

/// <reference path="abstracts/TwoDimensionalFigure.ts"/>
/// <reference path="abstracts/ThreeDimensionalFigure.ts"/>
/// <reference path="collections/FigureCollection.ts"/>
/// <reference path="figures/Rectangle.ts"/>
/// <reference path="figures/Cube.ts"/>

namespace App {
  const myTwoDimensionalFigureCollection =
    new FigureCollection<TwoDimensionalFigure>([
      new Rectangle('RedRectangle', 'red', 10, 5),
      new Rectangle('GreenRectangle', 'green', 5, 30),
    ]);

  const myThreeDimensionalFigureCollection =
    new FigureCollection<ThreeDimensionalFigure>([
      new Cube('RedCube', 'red', 10, 5, 4),
      new Cube('GreenCube', 'green', 5, 30, 7),
    ]);

  myTwoDimensionalFigureCollection.print();
  myThreeDimensionalFigureCollection.print();
}

Una vez hecho lo anterior y tras volver a compilar, el programa se ejecutará correctamente. Es importante mencionar en este punto que el orden en el que se han indicado las directivas reference-path es significativo. Por ejemplo, considere el caso en el que el contenido de index.ts fuera el siguiente:

/// <reference path="figures/Rectangle.ts"/>
/// <reference path="figures/Cube.ts"/>
/// <reference path="abstracts/TwoDimensionalFigure.ts"/>
/// <reference path="abstracts/ThreeDimensionalFigure.ts"/>
/// <reference path="collections/FigureCollection.ts"/>

namespace App {
  const myTwoDimensionalFigureCollection =
    new FigureCollection<TwoDimensionalFigure>([
      new Rectangle('RedRectangle', 'red', 10, 5),
      new Rectangle('GreenRectangle', 'green', 5, 30),
    ]);

  const myThreeDimensionalFigureCollection =
    new FigureCollection<ThreeDimensionalFigure>([
      new Cube('RedCube', 'red', 10, 5, 4),
      new Cube('GreenCube', 'green', 5, 30, 7),
    ]);

  myTwoDimensionalFigureCollection.print();
  myThreeDimensionalFigureCollection.print();
}

En este caso, tras compilar el código, al intentar ejecutar el programa surge el siguiente error:

$node dist/app.js 
/home/usuario/DSI/theory-examples/dist/app.js:4
    class Rectangle extends App.TwoDimensionalFigure {
                                ^

TypeError: Class extends value undefined is not a constructor or null
    at /home/usuario/DSI/theory-examples/dist/app.js:4:33
    at Object.<anonymous> (/home/usuario/DSI/theory-examples/dist/app.js:25:3)
    at Module._compile (node:internal/modules/cjs/loader:1734:14)
    at Object..js (node:internal/modules/cjs/loader:1899:10)
    at Module.load (node:internal/modules/cjs/loader:1469:32)
    at Function._load (node:internal/modules/cjs/loader:1286:12)
    at TracingChannel.traceSync (node:diagnostics_channel:322:14)
    at wrapModuleLoad (node:internal/modules/cjs/loader:235:24)
    at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:151:5)
    at node:internal/main/run_main_module:33:47

Node.js v23.9.0

El código de los ficheros Rectangle.ts y Cube.ts es procesado por el compilador de TypeScript con anterioridad al código de los ficheros TwoDimensionalFigure.ts y ThreeDimensionalFigure.ts. Los dos primeros ficheros hacen referencia a código incluido en los dos segundos ficheros, respectivamente. Es por ello que se produce el error, al no haberse procesado TwoDimensionalFigure.ts con anterioridad a Rectangle.ts, en este caso concreto.

Módulos ES

A lo largo de su historia, JavaScript ha gestionado los módulos haciendo uso de diferentes formatos. Al mismo tiempo, desde su nacimiento, TypeScript ha soportado muchos de esos formatos. Durante todo este tiempo, el estándar de JavaScript ha adoptado un formato denominado módulos ES (o ES6, dado que se incluyeron a partir de la sexta versión del estándar ECMAScript en 2015).

En TypeScript, al igual que en JavaScript (a partir de ES6), cualquier fichero que contenga una primera sentencia import o una sentencia export se considera un módulo. Un fichero sin lo anterior se trata como un script cuyo contenido se encuentra disponible en el ámbito global. Un módulo, sin embargo, tiene su propio ámbito local de ejecución. Aquellas variables, funciones, clases, etc., definidas en un módulo no serán visibles desde fuera del mismo, a menos que sean exportadas de manera explícita. Igualmente, para utilizar una variable, función, clase, interfaz, etc., exportada desde un módulo diferente, la misma tendrá que importarse explícitamente allá donde quiera ser utilizada.

Hay tres aspectos fundamentales que se deben tener en cuenta a la hora de escribir código modular en TypeScript:

  • Sintaxis. ¿Qué sintaxis puedo utilizar para importar/exportar?
  • Resolución de módulos. ¿Qué relación existe entre el nombre y directorio que utilizo en el código fuente para referirme a un módulo y el fichero correspondiente en disco?
  • Carga y ejecución de módulos. ¿Cómo va a ser cargado y ejecutado el código JavaScript generado perteneciente a un módulo? Este aspecto tiene que ver con la apariencia del código JavaScript generado por el compilador de TypeScript.

Respondiendo a la primera pregunta, en nuestro caso, utilizaremos la sintaxis de módulos ES, esto es, hacer uso de import y export. También se podría utilizar sintaxis CommonJS (es la que utilizan, al menos en versiones antiguas, la gran mayoría de los módulos disponibles a través de npm), la cual se basa en require() y module.exports = {}.

En lo que respecta a la segunda pregunta, la resolución de módulos es el proceso por el cual, a partir de una cadena de caracteres que acompaña, por ejemplo, a una sentencia import, se determina a qué fichero en disco se refiere dicha cadena. TypeScript incluye dos estrategias de resolución de módulos configurables a partir de la opción moduleResolution del compilador:

  1. Clásica. La opción moduleResolution toma por defecto el valor classic cuando la opción module es diferente al valor commonjs. Se incluye para proporcionar retrocompatibilidad.
  2. Node. La opción moduleResolution toma por defecto el valor node cuando la opción module toma el valor commonjs. Esta estrategia de resolución (en tiempo de compilación) replica la manera en la que Node.js utiliza CommonJS como resolutor y cargador de módulos (en tiempo de ejecución).

Para responder a la tercera y última pregunta, se deben tener en cuenta dos opciones del compilador de TypeScript que afectan a la apariencia del código JavaScript generado:

  • target. Determina para qué versión del estándar ECMAScript se va a generar el código JavaScript. Generalmente, el valor que tome esta propiedad dependerá del entorno de ejecución donde se va a ejecutar el código JavaScript generado, para lo que se deberá tener en cuenta, por ejemplo, aspectos como el navegador o versión de Node.js más antigua donde debería poder ejecutarse.
  • module. Determina el código JavaScript que debe generarse para que un módulo pueda interactuar con otro en tiempo de ejecución. Toda comunicación entre módulos se establece a través de un cargador de módulos que, en tiempo de ejecución, se encarga de localizar y ejecutar el código de un módulo.

En el caso que nos ocupa, hemos estado generando código para uno de los últimos estándares ECMAScript (ES2024), el cual está soportado por las últimas versiones de Node.js, además de hacer uso de CommonJS como cargador de módulos, que es el utilizado por Node.js. Lo anterior se corresponde con fijar el valor ES2024 a la propiedad target y el valor CommonJS a la propiedad module.

Una vez dicho todo lo anterior, continuemos con el ejemplo de la sección anterior. Para ello, en primer lugar, modifiquemos todos los ficheros de código fuente incluidos en el directorio src para eliminar el espacio de nombres App. También, eliminemos todas las directivas reference-path del fichero index.ts. Por último, deberemos tener las siguientes opciones del compilador habilitadas:

{
  "exclude": [
    "tests"
    "node_modules",
    "dist",
  ],
  "compilerOptions": {
    "target": "ES2024",
    "module": "CommonJS",
    "rootDir": "./src",
    "declaration": true,
    "outDir": "./dist",
    "strict": true,
  }
}

Lo anterior hará que el compilador de TypeScript informe de múltiples errores al tratar de recompilar el código fuente:

src/abstracts/ThreeDimensionalFigure.ts:4:20 - error TS2304: Cannot find name 'ColorType'.

4     private color: ColorType,
                     ~~~~~~~~~

src/abstracts/ThreeDimensionalFigure.ts:13:19 - error TS2304: Cannot find name 'ColorType'.

13   setColor(color: ColorType) {
                     ~~~~~~~~~

src/abstracts/TwoDimensionalFigure.ts:4:20 - error TS2304: Cannot find name 'ColorType'.

4     private color: ColorType,
                     ~~~~~~~~~

src/abstracts/TwoDimensionalFigure.ts:13:19 - error TS2304: Cannot find name 'ColorType'.

13   setColor(color: ColorType) {
                     ~~~~~~~~~

src/collections/FigureCollection.ts:2:13 - error TS2304: Cannot find name 'TwoDimensionalFigure'.

2   T extends TwoDimensionalFigure | ThreeDimensionalFigure,
              ~~~~~~~~~~~~~~~~~~~~

src/collections/FigureCollection.ts:2:36 - error TS2304: Cannot find name 'ThreeDimensionalFigure'.

...

Found 20 errors in 6 files.

Errors  Files
     2  src/abstracts/ThreeDimensionalFigure.ts:4
     2  src/abstracts/TwoDimensionalFigure.ts:4
     2  src/collections/FigureCollection.ts:2
     3  src/figures/Cube.ts:1
     3  src/figures/Rectangle.ts:1
     8  src/index.ts:2

Para solucionar estos errores, lo que debemos hacer es usar sentencias import para importar todas aquellas entidades de código exportadas que necesitemos utilizar en cada uno de los ficheros del directorio src. Por ejemplo, comencemos con el fichero ThreeDimensionalFigure.ts ubicado en el subdirectorio abstracts de src:

import { ColorType } from "../types/ColorType";

export abstract class ThreeDimensionalFigure {
  constructor(
    private readonly name: string,
    private color: ColorType,
  ) {}

  getName() {
    return this.name;
  }
  getColor() {
    return this.color;
  }
  setColor(color: ColorType) {
    this.color = color;
  }

  abstract getVolume(): number;
  abstract print(): void;
}

Se puede observar como hemos importado el tipo de datos propio ColorType que se encuentra definido en el fichero ColorType.ts del subdirectorio types de src. También es importante mencionar que debemos mantener la palabra reservada export en todas aquellas entidades de código que queramos utilizar fuera del fichero en el que se encuentran definidas.

De un modo similar, el contenido del fichero TwoDimensionalFigure.ts del directorio abstracts quedaría como sigue:

import { ColorType } from "../types/ColorType";

export 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;
}

En el caso del fichero src/collections/FigureCollections.ts, su contenido quedará tal y como sigue:

import { TwoDimensionalFigure } from "../abstracts/TwoDimensionalFigure";
import { ThreeDimensionalFigure } from "../abstracts/ThreeDimensionalFigure";

export class FigureCollection<
  T extends TwoDimensionalFigure | ThreeDimensionalFigure,
> {
  constructor(private figures: T[]) {}

  addFigure(newFigure: T) {
    this.figures.push(newFigure);
  }

  getNumberOfFigures() {
    return this.figures.length;
  }

  getFigure(index: number) {
    return this.figures[index];
  }

  print() {
    this.figures.forEach((figure) => {
      figure.print();
    });
  }
}

El contenido del fichero src/figures/Cube.ts debería ser el siguiente:

import { ThreeDimensionalFigure } from "../abstracts/ThreeDimensionalFigure";
import { ColorType } from "../types/ColorType";

export class Cube extends ThreeDimensionalFigure {
  private readonly faces = 6;

  constructor(
    name: string,
    color: ColorType,
    private base: number = 1,
    private height: number = 1,
    private depth: number = 1,
  ) {
    super(name, color);
  }

  getFaces() {
    return this.faces;
  }

  getVolume() {
    return this.base * this.height * this.depth;
  }

  print() {
    console.log(
      `I am a ${this.getName()}, I have ${this.getFaces()} faces, ` +
        `and my volume is ${this.getVolume()}`,
    );
  }
}

De un modo similar, el contenido del fichero src/figures/Rectangle.ts debería quedar tal y como sigue:

import { TwoDimensionalFigure } from "../abstracts/TwoDimensionalFigure";
import { ColorType } from "../types/ColorType";

export 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()}`,
    );
  }
}

El contenido del fichero src/index.ts deberá ser el siguiente:

import { FigureCollection } from "./collections/FigureCollection";
import { TwoDimensionalFigure } from "./abstracts/TwoDimensionalFigure";
import { Rectangle } from "./figures/Rectangle";
import { ThreeDimensionalFigure } from "./abstracts/ThreeDimensionalFigure";
import { Cube } from "./figures/Cube";

const myTwoDimensionalFigureCollection =
  new FigureCollection<TwoDimensionalFigure>([
    new Rectangle("RedRectangle", "red", 10, 5),
    new Rectangle("GreenRectangle", "green", 5, 30),
  ]);

const myThreeDimensionalFigureCollection =
  new FigureCollection<ThreeDimensionalFigure>([
    new Cube("RedCube", "red", 10, 5, 4),
    new Cube("GreenCube", "green", 5, 30, 7),
  ]);

myTwoDimensionalFigureCollection.print();
myThreeDimensionalFigureCollection.print();

Si recompilamos el código fuente, el directorio dist debería contener una estructura similar al directorio src:

[~/theory-examples(main)]$ls -l dist/
total 24
drwxrwxr-x 2 usuario usuario 4096 abr  6 20:30 abstracts
drwxrwxr-x 2 usuario usuario 4096 abr  6 20:30 collections
drwxrwxr-x 2 usuario usuario 4096 abr  6 20:30 figures
-rw-rw-r-- 1 usuario usuario   11 abr  6 20:30 index.d.ts
-rw-rw-r-- 1 usuario usuario  569 abr  6 20:30 index.js
drwxrwxr-x 2 usuario usuario 4096 abr  6 20:30 types

Además, la ejecución del programa muestra lo siguiente por la consola:

[~/theory-examples(main)]$node dist/index.js 
I am a RedRectangle, I have 4 sides, and my area is 50
I am a GreenRectangle, I have 4 sides, and my area is 150
I am a RedCube, I have 6 faces, and my volume is 200
I am a GreenCube, I have 6 faces, and my volume is 1050

El contenido del fichero index.js generado debería ser algo como lo que sigue:

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const FigureCollection_1 = require("./collections/FigureCollection");
const Rectangle_1 = require("./figures/Rectangle");
const Cube_1 = require("./figures/Cube");
const myTwoDimensionalFigureCollection = new FigureCollection_1.FigureCollection([
    new Rectangle_1.Rectangle("RedRectangle", "red", 10, 5),
    new Rectangle_1.Rectangle("GreenRectangle", "green", 5, 30),
]);
const myThreeDimensionalFigureCollection = new FigureCollection_1.FigureCollection([
    new Cube_1.Cube("RedCube", "red", 10, 5, 4),
    new Cube_1.Cube("GreenCube", "green", 5, 30, 7),
]);
myTwoDimensionalFigureCollection.print();
myThreeDimensionalFigureCollection.print();

Si modificamos la opción module del compilador para asignarle el valor AMD, el código generado en index.js será diferente:

define(["require", "exports", "./collections/FigureCollection", "./figures/Rectangle", "./figures/Cube"], function (require, exports, FigureCollection_1, Rectangle_1, Cube_1) {
    "use strict";
    Object.defineProperty(exports, "__esModule", { value: true });
    const myTwoDimensionalFigureCollection = new FigureCollection_1.FigureCollection([
        new Rectangle_1.Rectangle("RedRectangle", "red", 10, 5),
        new Rectangle_1.Rectangle("GreenRectangle", "green", 5, 30),
    ]);
    const myThreeDimensionalFigureCollection = new FigureCollection_1.FigureCollection([
        new Cube_1.Cube("RedCube", "red", 10, 5, 4),
        new Cube_1.Cube("GreenCube", "green", 5, 30, 7),
    ]);
    myTwoDimensionalFigureCollection.print();
    myThreeDimensionalFigureCollection.print();
});

Si ahora modificamos dicha opción para que tome el valor ES2022, esta vez el código generado en index.js tendrá la siguiente apariencia (el cual es muy similar al propio código a partir del cual se ha generado, esto es, el contenido en index.ts):

import { FigureCollection } from "./collections/FigureCollection";
import { Rectangle } from "./figures/Rectangle";
import { Cube } from "./figures/Cube";
const myTwoDimensionalFigureCollection = new FigureCollection([
    new Rectangle("RedRectangle", "red", 10, 5),
    new Rectangle("GreenRectangle", "green", 5, 30),
]);
const myThreeDimensionalFigureCollection = new FigureCollection([
    new Cube("RedCube", "red", 10, 5, 4),
    new Cube("GreenCube", "green", 5, 30, 7),
]);
myTwoDimensionalFigureCollection.print();
myThreeDimensionalFigureCollection.print();

Tal y como comentamos, dependiendo del entorno donde queramos ejecutar el código generado, tendremos que modificar la anterior propiedad. Por ejemplo, al intentar ejecutar el código generado en Node.js haciendo uso de los dos últimos valores de la propiedad module, el programa falla, dado que el cargador de módulos de Node.js (CommonJS) no es capaz de resolver y ejecutar los módulos correspondientes.

Sintaxis alternativa en la importación y exportación de módulos

Respecto a la importación de módulos, se puede utilizar una sintaxis alternativa, tal y como muestra el siguiente ejemplo de contenido del fichero index.ts:

import * as TwoDim from "./abstracts/TwoDimensionalFigure";
import { ThreeDimensionalFigure } from "./abstracts/ThreeDimensionalFigure";
import { Rectangle as Rect } from "./figures/Rectangle";
import { Cube } from "./figures/Cube";
import { FigureCollection } from "./collections/FigureCollection";

const myTwoDimensionalFigureCollection =
  new FigureCollection<TwoDim.TwoDimensionalFigure>([
    new Rect("RedRectangle", "red", 10, 5),
    new Rect("GreenRectangle", "green", 5, 30),
  ]);

const myThreeDimensionalFigureCollection =
  new FigureCollection<ThreeDimensionalFigure>([
    new Cube("RedCube", "red", 10, 5, 4),
    new Cube("GreenCube", "green", 5, 30, 7),
  ]);

myTwoDimensionalFigureCollection.print();
myThreeDimensionalFigureCollection.print();

Hemos modificado la sintaxis del primer import del anterior ejemplo:

import * as TwoDim from "./abstracts/TwoDimensionalFigure";

La anterior sintaxis indica que, todas las entidades de código exportadas pertenecientes al fichero ./abstracts/TwoDimensionalFigure.ts deben importarse en conjunto. Luego, para referirnos a alguna de dichas entidades, deberemos prefijar el alias TwoDim seguido de un punto y el identificador de dicha entidad:

  new FigureCollection<TwoDim.TwoDimensionalFigure>([

Los alias se pueden utilizar de otro modo, tal y como se muestra en la siguiente línea:

import { Rectangle as Rect } from "./figures/Rectangle";

En este caso, en lugar de utilizar Rectangle para referirnos a dicha clase, podremos hacerlo utilizando Rect, dentro del fichero donde la hemos importado:

    new Rect("RedRectangle", "red", 10, 5),
    new Rect("GreenRectangle", "green", 5, 30),

Por último, es importante mencionar que, por cada fichero, se puede definir una exportación por defecto. Por ejemplo, podemos modificar el fichero src/figures/Cube.ts para incluir dicha exportación por defecto:

import { ThreeDimensionalFigure } from "../abstracts/ThreeDimensionalFigure";
import { ColorType } from "../types/ColorType";

export default class Cube extends ThreeDimensionalFigure {
  private readonly faces = 6;

  constructor(
    name: string,
    color: ColorType,
    private base: number = 1,
    private height: number = 1,
    private depth: number = 1,
  ) {
    super(name, color);
  }

  getFaces() {
    return this.faces;
  }

  getVolume() {
    return this.base * this.height * this.depth;
  }

  print() {
    console.log(
      `I am a ${this.getName()}, I have ${this.getFaces()} faces, ` +
        `and my volume is ${this.getVolume()}`,
    );
  }
}

Ahora, a la hora de importar la clase Cube en otro fichero diferente, por ejemplo, en el fichero src/index.ts, podremos hacer algo como lo siguiente:

import * as TwoDim from "./abstracts/TwoDimensionalFigure";
import { ThreeDimensionalFigure } from "./abstracts/ThreeDimensionalFigure";
import { Rectangle as Rect } from "./figures/Rectangle";
import MyCube from "./figures/Cube";
import { FigureCollection } from "./collections/FigureCollection";

const myTwoDimensionalFigureCollection =
  new FigureCollection<TwoDim.TwoDimensionalFigure>([
    new Rect("RedRectangle", "red", 10, 5),
    new Rect("GreenRectangle", "green", 5, 30),
  ]);

const myThreeDimensionalFigureCollection =
  new FigureCollection<ThreeDimensionalFigure>([
    new MyCube("RedCube", "red", 10, 5, 4),
    new MyCube("GreenCube", "green", 5, 30, 7),
  ]);

myTwoDimensionalFigureCollection.print();
myThreeDimensionalFigureCollection.print();

En concreto, hemos modificado la siguiente línea:

import MyCube from "./figures/Cube";

En este caso, al no usar {} estamos indicando que queremos importar la exportación por defecto y, además, le estamos dando el alias MyCube. Es por ello que, en el código, debemos hacer lo siguiente para referirnos a la clase Cube:

    new MyCube("RedCube", "red", 10, 5, 4),
    new MyCube("GreenCube", "green", 5, 30, 7),

Módulos ECMAScript (ESM) en Node.js

Durante los últimos años, se ha intentado que Node.js proporcione soporte para ejecutar módulos ESM, lo cual ha sido bastante complicado dado que, como ya se ha indicado con anterioridad, tradicionalmente, Node.js siempre ha utilizado el sistema CommonJS (CJS).

En la sección anterior, al hacer uso del valor CommonJS en la opción module del compilador, realmente, hemos estado generando módulos CJS.

No obstante, ahora que Node.js proporciona la posibilidad de ejecutar directamente módulos ESM, las opciones moduleResolution y module del compilador de TypeScript también pueden tomar los valores Node16 y NodeNext.

Al mismo tiempo, Node.js también soporta una nueva opción en el fichero package.json, denominada type que puede tomar los valores module o commonjs. La opción permite establecer si los ficheros de código JavaScript (.js) generados deben interpretarse como módulos ESM o como módulos CJS en Node.js. Por defecto, si no está establecida, los ficheros se interpretarán como módulos CJS.

Existen diferencias en la gestión de un módulo ESM y un módulo CJS, siendo una de las más importantes la forma en la que se deben importar artefactos en nuestro código escrito en TypeScript.

Por ejemplo, cuando TypeScript encuentra un fichero con extensión .ts, va a intentar averiguar, a través del fichero package.json, si ese fichero debe interpretarse como un módulo ESM y, por lo tanto, cómo poder encontrar otros módulos que ese fichero importe, además de cómo generar adecuadamente el código JavaScript correspondiente.

Cuando un fichero .ts se compila como un módulo ESM, la sintaxis import/export se dejará tal cual en el fichero .js correspondiente. Sin embargo, cuando se compila como un módulo CJS, el código generado será muy similar a aquel que se obtiene fijando el valor CommonJS a la opción module de la configuración del compilador.

Continuando con el ejemplo de secciones anteriores, modifiquemos nuestro fichero package.json para que contenga la opción type fijada al valor commonjs. También, modifiquemos la opción module del fichero de configuración del compilador para que tome el valor Node16. Al intentar compilar, TypeScript interpretará todos los ficheros .ts como módulos CJS (debido a la opción type del fichero package.json). Por lo tanto, el código JavaScript generado será muy similar al generado utilizando el valor CommonJS para la opción module de la configuración del compilador (aunque realmente dicha opción tenga asignado el valor Node16). Lo anterior hará que el código fuente generado se siga ejecutando correctamente en Node.js. Véase, por ejemplo, el código generado en el fichero index.js:

"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const Rectangle_1 = require("./figures/Rectangle");
const Cube_1 = __importDefault(require("./figures/Cube"));
const FigureCollection_1 = require("./collections/FigureCollection");
const myTwoDimensionalFigureCollection = new FigureCollection_1.FigureCollection([
    new Rectangle_1.Rectangle("RedRectangle", "red", 10, 5),
    new Rectangle_1.Rectangle("GreenRectangle", "green", 5, 30),
]);
const myThreeDimensionalFigureCollection = new FigureCollection_1.FigureCollection([
    new Cube_1.default("RedCube", "red", 10, 5, 4),
    new Cube_1.default("GreenCube", "green", 5, 30, 7),
]);
myTwoDimensionalFigureCollection.print();
myThreeDimensionalFigureCollection.print();

Ahora bien, modifiquemos la opción type del fichero package.json para que tome el valor module. En este caso, el compilador emite los siguientes errores:

src/abstracts/ThreeDimensionalFigure.ts:1:27 - error TS2835: Relative import paths need explicit file extensions in ECMAScript imports when '--moduleResolution' is 'node16' or 'nodenext'. Did you mean '../types/ColorType.js'?

1 import { ColorType } from "../types/ColorType";
                            ~~~~~~~~~~~~~~~~~~~~

src/abstracts/TwoDimensionalFigure.ts:1:27 - error TS2835: Relative import paths need explicit file extensions in ECMAScript imports when '--moduleResolution' is 'node16' or 'nodenext'. Did you mean '../types/ColorType.js'?

1 import { ColorType } from "../types/ColorType";
                            ~~~~~~~~~~~~~~~~~~~~

src/collections/FigureCollection.ts:1:38 - error TS2835: Relative import paths need explicit file extensions in ECMAScript imports when '--moduleResolution' is 'node16' or 'nodenext'. Did you mean '../abstracts/TwoDimensionalFigure.js'?

1 import { TwoDimensionalFigure } from "../abstracts/TwoDimensionalFigure";
                                       ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

src/collections/FigureCollection.ts:2:40 - error TS2835: Relative import paths need explicit file extensions in ECMAScript imports when '--moduleResolution' is 'node16' or 'nodenext'. Did you mean '../abstracts/ThreeDimensionalFigure.js'?

...

Found 15 errors in 6 files.

Errors  Files
     1  src/abstracts/ThreeDimensionalFigure.ts:1
     1  src/abstracts/TwoDimensionalFigure.ts:1
     2  src/collections/FigureCollection.ts:1
     3  src/figures/Cube.ts:1
     3  src/figures/Rectangle.ts:1
     5  src/index.ts:1

Ahora TypeScript interpreta que todos los ficheros .ts se tratan de módulos ESM. Además, al haber fijado module al valor Node16, la opción moduleResolution ha tomado el mismo valor implícitamente. La resolución de módulos ESM es diferente a la resolución de módulos CJS y, por lo tanto, el compilador emite los errores anteriores, indicando que en las sentencias import se debe hacer uso de la extensión .js. Añadiendo dichas extensiones en todas y cada una de las sentencias import de los ficheros ubicados en el directorio src, el compilador dejará de emitir dichos mensajes de error y, además, el código fuente generado se ejecutará correctamente en Node.js. Además, obsérvese el código generado para el fichero index.js, el cual difiere del generado con anterioridad:

import { Rectangle as Rect } from "./figures/Rectangle.js";
import MyCube from "./figures/Cube.js";
import { FigureCollection } from "./collections/FigureCollection.js";
const myTwoDimensionalFigureCollection = new FigureCollection([
    new Rect("RedRectangle", "red", 10, 5),
    new Rect("GreenRectangle", "green", 5, 30),
]);
const myThreeDimensionalFigureCollection = new FigureCollection([
    new MyCube("RedCube", "red", 10, 5, 4),
    new MyCube("GreenCube", "green", 5, 30, 7),
]);
myTwoDimensionalFigureCollection.print();
myThreeDimensionalFigureCollection.print();

Por último, cabe mencionar que Node.js ahora soporta dos extensiones nuevas: .mjs y .cjs. Node.js siempre interpretará los ficheros con la primera extensión como módulos ESM, mientras que los interpretará como módulos CJS haciendo uso de la segunda extensión, con independencia de lo que se haya establecido en la opción type del fichero package.json.

Del mismo modo, TypeScript ahora soporta dos nuevas extensiones para los ficheros con código fuente: .mts y .cts. Al compilar ficheros con estas extensiones, el compilador generará ficheros con extensiones .mjs y .cjs, respectivamente, y cuyo contenido también será diferente según el caso.