Módulos

Módulos en TypeScript

JavaScript tiene una larga historia de diferentes formas de manejar el código modularizado. TypeScript existe desde 2012 y ha implementado soporte para muchos de estos formatos, pero con el tiempo la comunidad y la especificación de JavaScript han convergido en un formato llamado ES Modules (o módulos ES6). Quizás la conozcas como la sintaxis importa/export.

ES Modules se agregó a la especificación de JavaScript en 2015 y, para 2020, tenía un amplio soporte en la mayoría de los navegadores web y runtimes de JavaScript.

Para enfocarte, el manual cubrirá tanto los módulos ES como su popular precursor CommonJS con sintaxis module.exports =, y puedes encontrar información sobre los otros patrones de módulos en la sección de referencia en Módulos ↗.

Cómo se definen los módulos de JavaScript

En TypeScript, al igual que en ECMAScript 2015, cualquier archivo que contenga un import o export de nivel superior se considera un módulo.

Por el contrario, un archivo sin ninguna declaración de importación o exportación de nivel superior se trata como un script cuyo contenido está disponible en el ámbito global (y, por lo tanto, también para los módulos).

Los módulos se ejecutan dentro de su propio alcance, no en el alcance global. Esto significa que las variables, funciones, clases, etc. declaradas en un módulo no son visibles fuera del módulo a menos que se exporten explícitamente utilizando una de las formas de exportación. Por el contrario, para consumir una variable, función, clase, interfaz, etc. exportada desde un módulo diferente, debe importarse utilizando uno de las formas de importación.

Lo que no es un módulo

Antes de comenzar, es importante comprender qué considera TypeScript un módulo. La especificación de JavaScript declara que cualquier archivo JavaScript sin una declaración import, export o await de nivel superior debe considerarse un script y no un módulo.

Dentro de un archivo de script, las variables y los tipos se declaran en el alcance global compartido, y se supone que usarás la opción outFile del compilador para unir múltiples archivos de entrada en un archivo de salida, o usa múltiples etiquetas <script> en tu HTML para cargar estos archivos (¡en el orden correcto!).

Si tienes un archivo que actualmente no tiene ningun import o export, pero quieres que sea tratado como un módulo, agrega la línea:

Prueba este código ↗

export {};

lo que cambiará el archivo para que sea un módulo que no exporta nada. Esta sintaxis funciona independientemente del objetivo de tu módulo.

Módulos en TypeScript

Lectura adicional: Impatient JS (Modules) ↗ y MDN : Módulos de JavaScript ↗

Hay tres cosas principales a considerar al escribir código basado en módulos en TypeScript:

  • Sintaxis: ¿Qué sintaxis quiero usar para importar y exportar cosas?
  • Resolución del módulo: ¿Cuál es la relación entre los nombres (o rutas) de los módulos y los archivos en el disco?
  • Objetivo de salida del módulo: ¿Cómo debería verse mi módulo JavaScript emitido?

Sintaxis de ES Module

Un archivo puede declarar una exportación principal a través de export default:

Prueba este código ↗

// @filename: hello.ts
export default function helloWorld() {
  console.log("Hello, world!");
}

Esto luego se importa a través de:

Prueba este código ↗

import helloWorld from "./hello.js";
helloWorld();

Además de la exportación predeterminada, puedes tener más de una exportación de variables y funciones a través de export omitiendo default:

Prueba este código ↗

// @filename: maths.ts
export var pi = 3.14;
export let squareTwo = 1.41;
export const phi = 1.61;
 
export class RandomNumberGenerator {}
 
export function absolute(num: number) {
  if (num < 0) return num * -1;
  return num;
}

Estos se pueden usar en otro archivo mediante la sintaxis import:

Prueba este código ↗

import { pi, phi, absolute } from "./maths.js";
 
console.log(pi);
const absPhi = absolute(phi);
        
const absPhi: number

Sintaxis de importación adicional

Se puede cambiar el nombre de una importación usando un formato como import {old as new}:

Prueba este código ↗

import { pi as π } from "./maths.js";
 
console.log(π);
           
(alias) var π: number
import π

Puedes mezclar y combinar la sintaxis anterior en un solo import:

Prueba este código ↗

// @filename: maths.ts
export const pi = 3.14;
export default class RandomNumberGenerator {}
 
// @filename: app.ts
import RandomNumberGenerator, { pi as π } from "./maths.js";
 
RandomNumberGenerator;
         
(alias) class RandomNumberGenerator
import RandomNumberGenerator
 
console.log(π);
           
(alias) const π: 3.14
import π

Puedes tomar todos los objetos exportados y colocarlos en un único espacio de nombres usando * as name:

Prueba este código ↗

// @filename: app.ts
import * as math from "./maths.js";
 
console.log(math.pi);
const positivePhi = math.absolute(math.phi);
          
const positivePhi: number

Puedes importar un archivo y no incluir ninguna variable en tu módulo actual a través de import "./file":

Prueba este código ↗

// @filename: app.ts
import "./maths.js";
 
console.log("3.14");

En este caso, el import no hace nada. Sin embargo, se evaluó todo el código en maths.ts, lo que podría provocar efectos secundarios que afecten a otros objetos.

Sintaxis específica de ES Module de TypeScript

Los tipos se pueden exportar e importar usando la misma sintaxis que los valores de JavaScript:

Prueba este código ↗

// @filename: animal.ts
export type Cat = { breed: string; yearOfBirth: number };
 
export interface Dog {
  breeds: string[];
  yearOfBirth: number;
}
 
// @filename: app.ts
import { Cat, Dog } from "./animal.js";
type Animals = Cat | Dog;

TypeScript ha ampliado la sintaxis import con dos conceptos para declarar la importación de un tipo:

import type

Que es una declaración de importación que solo puede importar tipos:

Prueba este código ↗

// @filename: animal.ts
export type Cat = { breed: string; yearOfBirth: number };
export type Dog = { breeds: string[]; yearOfBirth: number };
export const createCatName = () => "fluffy";
 
// @filename: valid.ts
import type { Cat, Dog } from "./animal.js";
export type Animals = Cat | Dog;
 
// @filename: app.ts
import type { createCatName } from "./animal.js";
const name = createCatName();
Error generado
'createCatName' cannot be used as a value because it was imported using 'import type'.
Importaciones type en línea

TypeScript 4.5 también permite que las importaciones individuales tengan el prefijo type para indicar que la referencia importada es un tipo:

Prueba este código ↗

// @filename: app.ts
import { createCatName, type Cat, type Dog } from "./animal.js";
 
export type Animals = Cat | Dog;
const name = createCatName();

Juntos permiten que un transpilador que no sea TypeScript como Babel, swc o esbuild sepa qué importaciones se pueden eliminar de forma segura.

Sintaxis de ES Module con comportamiento CommonJS

TypeScript tiene una sintaxis de ES Module que directamente se correlaciona con un require de CommonJS y AMD. Las importaciones usando ES Module son en la mayoría de los casos iguales que el require de esos entornos, pero esta sintaxis garantiza que tengas una coincidencia 1 a 1 en tu archivo TypeScript con la salida de CommonJS:

Prueba este código ↗

import fs = require("fs");
const code = fs.readFileSync("hello.ts", "utf8");

Puedes obtener más información sobre esta sintaxis en la página de referencia de módulos ↗.

Sintaxis CommonJS

CommonJS es el formato en el que se entregan la mayoría de los módulos de npm. Incluso si estás escribiendo usando la sintaxis de ES Module anterior, tener una breve comprensión de cómo funciona la sintaxis de CommonJS te ayudará a depurar más fácilmente.

Exportando

Los identificadores se exportan configurando la propiedad exports en un global llamado module.

Prueba este código ↗

function absolute(num: number) {
  if (num < 0) return num * -1;
  return num;
}
 
module.exports = {
  pi: 3.14,
  squareTwo: 1.41,
  phi: 1.61,
  absolute,
};

Entonces estos archivos se pueden importar mediante una declaración require:

Prueba este código ↗

const maths = require("./maths");
maths.pi;
      
any

O puedes simplificar un poco usando la función de desestructuración en JavaScript:

Prueba este código ↗

const { squareTwo } = require("./maths");
squareTwo;
   
const squareTwo: any

Interoperabilidad de CommonJS y ES Module

Existe una falta de coincidencia en las características entre CommonJS y ES Module con respecto a la distinción entre una importación predeterminada y una importación de objeto de espacio de nombres de módulo. TypeScript tiene un indicador de compilador para reducir la fricción entre los dos conjuntos diferentes de restricciones con esModuleInterop.

Opciones de resolución de módulo de TypeScript

La resolución de módulo es el proceso de tomar una cadena de la declaración import o require y determinar a qué archivo se refiere esa cadena.

TypeScript incluye dos estrategias de resolución: Classic y Node. Classic, el valor predeterminado cuando la opción del compilador module no es commonjs, se incluye para compatibilidad con versiones anteriores. La estrategia Node replica cómo funciona Node.js en modo CommonJS, con comprobaciones adicionales para .ts y .d.ts.

Hay muchos indicadores de TSConfig que influyen en la estrategia del módulo dentro de TypeScript: moduleResolution, baseUrl, paths, [rootDirs ↗] (https://www.typescriptlang.org/tsconfig.html#rootDirs).

Para obtener detalles completos sobre cómo funcionan estas estrategias, puedes consultar la página de referencia Resolución de módulo ↗.

Opciones de salida de módulo de TypeScript

Hay dos opciones que afectan la salida de JavaScript emitida:

  • target que determina qué características de JS se reducen de nivel (se convierten para ejecutarse en runtimes de JavaScript más antiguos) y que quedan intactos
  • module que determina qué código se utiliza para que los módulos interactúen entre sí

Qué target usas está determinado por las funciones disponibles en el runtime de JavaScript que esperas ejecutar. Eso podría ser: el navegador web más antiguo que admite, la versión más baja de Node.js que esperas ejecutar o podría provenir de restricciones únicas de tu runtime, como Electron, por ejemplo.

Toda la comunicación entre módulos ocurre a través de un cargador de módulos, la opción del compilador module determina cuál se usa. En tiempo de ejecución, el cargador de módulos es responsable de localizar y ejecutar todas las dependencias de un módulo antes de ejecutarlo.

Por ejemplo, aquí hay un archivo TypeScript que usa la sintaxis de ES Module, que muestra algunas opciones diferentes para module:

Prueba este código ↗

import { valueOfPi } from "./constants.js";
 
export const twoPi = valueOfPi * 2;

ES2020

Prueba este código ↗

import { valueOfPi } from "./constants.js";
export const twoPi = valueOfPi * 2;
 

CommonJS

Prueba este código ↗

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.twoPi = void 0;
const constants_js_1 = require("./constants.js");
exports.twoPi = constants_js_1.valueOfPi * 2;
 

UMD

Prueba este código ↗

(function (factory) {
    if (typeof module === "object" && typeof module.exports === "object") {
        var v = factory(require, exports);
        if (v !== undefined) module.exports = v;
    }
    else if (typeof define === "function" && define.amd) {
        define(["require", "exports", "./constants.js"], factory);
    }
})(function (require, exports) {
    "use strict";
    Object.defineProperty(exports, "__esModule", { value: true });
    exports.twoPi = void 0;
    const constants_js_1 = require("./constants.js");
    exports.twoPi = constants_js_1.valueOfPi * 2;
});
 

Ten en cuenta que ES2020 es efectivamente el mismo que el index.ts original.

Puedes ver todas las opciones disponibles y cómo se ve el código JavaScript emitido en la Referencia TSConfig para módule.

TypeScript namespaces

TypeScript tiene su propio formato de módulo llamado namespaces que es anterior al estándar de ES Module. Esta sintaxis tiene muchas características útiles para crear archivos de definición complejos y todavía se usa activamente en DefinitelyTyped ↗. Si bien no están obsoletas, la mayoría de las funciones en los espacios de nombres existen en ES Module y te recomendamos que las utilices para alinearte con la dirección de JavaScript. Puedes obtener más información sobre los espacios de nombres en la página de referencia de espacios de nombres ↗.

Última actualización