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:
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
:
// @filename: hello.ts
export default function helloWorld() {
console.log("Hello, world!");
}
Esto luego se importa a través de:
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
:
// @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
:
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}
:
import { pi as π } from "./maths.js";
console.log(π);
(alias) var π: number
import π
Puedes mezclar y combinar la sintaxis anterior en un solo import
:
// @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
:
// @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"
:
// @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:
// @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:
// @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();
'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:
// @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:
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
.
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
:
const maths = require("./maths");
maths.pi;
any
O puedes simplificar un poco usando la función de desestructuración en JavaScript:
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 intactosmodule
↗ 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
↗:
import { valueOfPi } from "./constants.js";
export const twoPi = valueOfPi * 2;
ES2020
import { valueOfPi } from "./constants.js";
export const twoPi = valueOfPi * 2;
CommonJS
"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
(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 ↗.