Módulos

Introducción

Una variable declarada fuera de cualquier bloque de código tiene alcance global. Lo mismo ocurre con las funciones y las clases. Este alcance global permite que sean accesibles durante todo el programa, pero plantea un problema si otra declaración (variable, función o clase) usa el mismo nombre. Este fenómeno, llamado colisión de nombres, puede provocar errores o comportamientos inesperados cuando se ejecuta el programa. El compilador de TypeScript detecta estas colisiones y genera un error durante la compilación.

Ejemplo:

// Compilation error TS2393:  
// Duplicate function implementation.  
function getSalary() {  
  return 10000;  
}  
  
// Compilation error TS2393:  
// Duplicate function implementation.  
function getSalary() {  
  return 10;  
}  
  
console.log(getSalary()); 

En este ejemplo, si se ejecuta el código, la última línea mostrará 10 en la consola. De hecho, la segunda declaración de la función getSalary() sobrescribe la anterior.

Por tanto, tener declaraciones con un alcance global es una muy mala práctica que dificulta considerablemente la escalabilidad del programa. Estamos hablando de contaminación del espacio global. Cuando se implementa esta mala práctica, resulta...

Histórico

1. Pattern module

Entre 2005 y 2010, AJAX se estandarizó y aparecieron numerosas bibliotecas JavaScript que ampliaron las capacidades del lenguaje: jQuery, Dojo, Prototype… Desde entonces, las aplicaciones web han ido utilizando cada vez más código JavaScript para dinamizar las páginas, pero las colisiones de nombres suponen un verdadero problema. El pattern module es la primera solución para encapsular y aislar declaraciones. Adoptado por la mayoría de las bibliotecas de la época, se basa principalmente en la capacidad de los closures (consulte el capítulo Tipos e instrucciones básicas - Funciones) y tiene diferentes implementaciones.

Para crear un módulo, se debe utilizar una Immediately Invoked Function Expression (expresión de función invocada inmediatamente, en español) más conocida bajo el alias IIFE. Una IIFE es una función anónima asignada a una variable, ejecutada inmediatamente.

Ejemplo:

const salariedModule = (function() {  
  const salary = 20_000;  
  
  return {  
    getSalary: function() {  
      return salary;  
    }  
  };  
})();  
  
const employeeModule = (function(salariedModule) {  
  return {  
    personList: ["Evelyn", "John"],  
    getSalary: function() {  
      return salariedModule.getSalary();  
    }  
 };  
})(salariedModule);  
  
const managerModule = (function(salariedModule) {  
  const bonus = 40_000;  
  return {  
    personList: ["Patrick", "David"],  
    getSalary: function() {  
      return salariedModule.getSalary() + bonus;  
    }  
  };  
})(salariedModule);  
  
// Log: 20 000  
console.log(employeeModule.getSalary());  ...

El estándar ECMAScript 2015

Los módulos definidos por el estándar ECMAScript 2015 aprovechan al máximo los estándares CommonJS y AMD. Al igual que CommonJS, el estándar ofrece una sintaxis simple que le permite exportar e importar datos desde un módulo mediante dos palabras clave:

  • export: permite exportar elementos.

  • import: permite importar elementos de un módulo.

Tan pronto como un archivo incluye una de estas palabras clave, se convierte en un  módulo. Por lo tanto, solo puede haber un módulo por archivo. Los módulos ECMAScript 2015 tienen como objetivo admitir la carga asíncrona y síncrona. También permiten gestionar dependencias cíclicas (aunque esto se considera una mala práctica).

Los módulos deben importarse fuera de cualquier bloque de código. Sin embargo, es posible importarlos dinámicamente mediante el uso de la función import(), que devuelve el módulo en forma de promesa (el concepto de promesa se abordará en el capítulo Asincronismo). Esta funcionalidad se ha estandarizado en la versión 2020 de ECMAScript.

Ejemplo (primer módulo):

/**  
 * salariedModule.js  
 */  
const salary = 20_000;  
  
export const salaried = {  
  getSalary: function() {  
    return salary;  
  }  
}; 

Ejemplo (segundo módulo):

/**  
 * employeeModule.js  
 */  
import { salaried } from "./salariedModule";  
  
export const employee = {  
  personList: ["Patrick", "David"],  
  getSalary: function() {  
    return salaried.getSalary();  
  }  
}; 

Ejemplo (tercer módulo):

/**  
 * managerModule.js  
 */  
import { salaried } from "./salariedModule";  
  
const bonus = 40_000;  
  
export const manager = {  
  personList: ["Patrick", "David"],  
  getSalary: function() {  
    return salaried.getSalary() + bonus;  ...

Gestión de tipos

Import type

Como se dijo con anterioridad, los tipos definidos en TypeScript solo son útiles en el momento de la compilación y se eliminan al transpilar al código JavaScript.

Ejemplo (exportación):

/**   
 * type.js  
 */   
export interface Employee {  
  // ...  
} 

Ejemplo (importación):

/**   
 * main.ts   
 */   
import { Employee } from "./type";  
  
const evelyn: Employee = {  
 // ...  
}; 

En este ejemplo, el archivo type.ts desaparecerá durante la transpilación, así como la línea de importación del archivo main.ts. Hay una notación que ayuda al compilador y hace que este comportamiento sea más explícito: import y export de tipos.

Sintaxis (exportación):

export type { element1, element2, ... } from "./path/to/module"; 

Ejemplo (exportación):

// type.ts  
export type interface Employee {  
  // ...  
} 

Sintaxis (importación):

import type { element1, element2, ... } from "./path/to/module"; 

Ejemplo (importación):

// main.ts  
import type { Employee } from "./type";  
 
 const evelyn: Employee...

Carga y resoluciones

1. Resoluciones

Los proyectos más recientes utilizan el concepto de módulo. Al importar un módulo, el compilador de TypeScript debe resolver el archivo que representa el módulo en el momento de la compilación. Lo mismo ocurre cuando el código lo ejecuta el motor JavaScript. TypeScript intentará resolver estas importaciones utilizando un algoritmo que se puede seleccionar mediante una opción de compilación llamada --moduleResolution. Una vez resuelto, el compilador puede proponer:

  • Autocompletado al importar el módulo.

  • Detección de errores.

  • Autocompletado al utilizar el módulo.

La opción de compilación --traceResolution permite obtener registros cuando el compilador de TypeScript intenta resolver las diferentes importaciones. Esto es útil para diagnosticar una importación que el compilador no puede resolver.

Hay dos formas de establecer la ruta a un módulo durante la importación:

  • Las importaciones con rutas relativas son módulos internos del proyecto. Usando esta ruta, el compilador de TypeScript intentará encontrar el archivo con la extensión .ts o .d.ts correspondiente a esta importación. Si no se encuentra, se generará un error de compilación.

Ejemplo:

import { employee } from "./employeeModule"; 
  • Las rutas de importación compuestas únicamente por un nombre son módulos que provienen de un paquete NPM y, por lo tanto, existen en la carpeta node_modules(que contiene todos los paquetes). Para resolver esta importación, el compilador buscará en la carpeta node_modulesel nombre de la carpeta correspondiente al nombre especificado durante la importación. Si lo encuentra, analizará el archivo package.json para encontrar el archivo de entrada del paquete para cargarlo. Si no encuentra el paquete, se generará un error de compilación.

Ejemplo:...