Menu

Módulos en TypeScript: import, export e import type

Todo archivo de TypeScript con un import o export de nivel superior es un módulo. Aprende los exports con nombre y por defecto, import type y export type, cómo la opción module decide entre salida ES module y CommonJS, y por qué node16 y nodenext piden la extensión .js en los imports.

Esta página incluye editores ejecutables: edita, ejecuta y ve el resultado al instante.

Un módulo de TypeScript es un archivo con al menos un import o export de nivel superior. Todo lo que se declara en él es privado del archivo salvo que lo exportes con export, y los demás archivos importan con import lo que necesitan. La sintaxis es la de los ES modules de JavaScript más unas cuantas formas solo para tipos.

Exports con nombre y por defecto

Un proyecto tiene muchos archivos, así que el lado que importa se ve así. El export por defecto se importa sin llaves y puede tomar cualquier nombre; los exports con nombre van entre llaves y conservan su nombre salvo que los renombres con as.

// main.ts
import describe, { distance, ORIGIN, type Point } from "./math.js";
import { distance as dist } from "./math.js"; // renamed on import
import * as math from "./math.js";            // everything, as one object

const p: Point = { x: 6, y: 8 };
console.log(describe(p), distance(ORIGIN, p), dist(p, p), math.ORIGIN);
FormaQué importa
import { a, b } from "./m.js"los exports con nombre a y b
import x from "./m.js"el export por defecto, con el nombre x
import * as m from "./m.js"un objeto namespace con todos los exports
import { a as b } from "./m.js"a, renombrado a b en este archivo
import type { T } from "./m.js"solo tipos, eliminados de la salida
import "./setup.js"ejecuta el archivo por sus efectos secundarios
export { a } from "./m.js"reexporta a sin importarlo
export * from "./m.js"reexporta todos los exports con nombre

Los reexports permiten que un archivo (a menudo index.ts) reúna la API pública de una carpeta. El comportamiento de los módulos de JavaScript en sí, como los live bindings y la caché de módulos, se explica en ES modules.

import type y export type

Los tipos no existen en tiempo de ejecución, así que un import que solo se usa como tipo no tiene nada que cargar. import type lo dice de forma explícita, y la sentencia desaparece de la salida en JavaScript. El modificador type también funciona sobre un único nombre dentro de un import normal.

import type { User } from "./models.js";        // whole statement erased
import { saveUser, type Settings } from "./api.js"; // only saveUser survives

export type { User };                            // re-export a type only
export type UserId = User["id"];

Sin la palabra clave, TypeScript igualmente elimina los nombres que ve que solo son tipos. Dos configuraciones hacen obligatoria la palabra clave:

  • verbatimModuleSyntax: true conserva todos los imports que no están marcados con type, así que un import de tipo sin marcar es el error TS1484: 'Point' is a type and must be imported using a type-only import when 'verbatimModuleSyntax' is enabled.
  • Ejecutar archivos .ts directamente en Node (type stripping) elimina las anotaciones de tipo pero no mira otros archivos. Un import { Point } sin marcar se queda en el código, y Node falla en tiempo de ejecución con SyntaxError: The requested module './math.ts' does not provide an export named 'Point'.

Escribir type en cada import que solo trae tipos funciona en los dos casos, así que es el hábito que conviene adquirir.

Cannot find module

Cuando la ruta de un import no lleva a un archivo ni a un paquete con tipos, el compilador se detiene con el error TS2307. Ejecuta este ejemplo para verlo:

La salida es index.ts(2,29): error TS2307: Cannot find module './utils.js' or its corresponding type declarations. Las causas habituales:

  • Una errata en una ruta relativa, o que falte el ./ (sin él, el nombre se busca en node_modules).
  • Un paquete de JavaScript sin tipos incluidos: instala @types/{package} si existe, o escribe un archivo de declaraciones para él.
  • Una subruta que el paquete no incluye en el campo exports de su package.json. Con la resolución node16, nodenext y bundler, solo se pueden importar los puntos de entrada listados.

Salida ES modules o CommonJS

Tú siempre escribes import y export. La opción module de tsconfig.json decide qué JavaScript sale.

// Source, the same in both cases
import { add } from "./math.js";
console.log(add(1, 2));
// module: nodenext, in a package with "type": "module"
import { add } from "./math.js";
console.log(add(1, 2));
// module: nodenext, in a package without "type": "module"
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const math_js_1 = require("./math.js");
console.log((0, math_js_1.add)(1, 2));

Con node16, node18, node20 o nodenext, TypeScript sigue las propias reglas de Node para cada archivo:

ArchivoFormato de salida
.ts en un paquete con "type": "module"ES module
.ts en un paquete sin élCommonJS
.mtssiempre ES module, generado como .mjs
.ctssiempre CommonJS, generado como .cjs

Con module: "esnext" o "preserve", la salida mantiene import/export, que es lo que esperan bundlers como Vite y esbuild. Las diferencias entre los dos sistemas de módulos en tiempo de ejecución están en CommonJS frente a ESM.

Extensiones de archivo en los imports

Con module: node16 o nodenext, un archivo ES module debe nombrar el archivo que Node cargará de verdad, y ese es el .js compilado. TypeScript asocia ./math.js de vuelta a math.ts para comprobar los tipos.

import { add } from "./math";    // error TS2835 in an ES module file
import { add } from "./math.js"; // correct: the path as it exists after compiling

El texto del error es Relative import paths need explicit file extensions in ECMAScript imports when '--moduleResolution' is 'node16' or 'nodenext'. Did you mean './math.js'? Los archivos CommonJS con la misma configuración pueden omitir la extensión, porque require prueba las extensiones por su cuenta.

Otras dos configuraciones cambian la regla:

  • moduleResolution: "bundler" (con module en esnext, preserve o commonjs) acepta ./math sin extensión, porque la resuelve el bundler.
  • rewriteRelativeImportExtensions: true te deja escribir ./math.ts, la misma ruta que funciona cuando Node ejecuta directamente el archivo .ts, y la reescribe a ./math.js en la salida.

Resolución de módulos y paths

Para un nombre sin ruta como "zod", TypeScript busca en node_modules, lee el package.json del paquete (sus campos exports y types) y, si no, recurre a node_modules/@types/zod. Los nombres relativos (./, ../) se resuelven desde el archivo que importa. También se puede importar un archivo .json; la configuración que necesita está en la página de JSON.

paths en tsconfig.json añade alias para tus propias carpetas:

{
    "compilerOptions": {
        "module": "esnext",
        "moduleResolution": "bundler",
        "paths": {
            "@lib/*": ["./src/lib/*"]
        }
    }
}

paths solo afecta a la comprobación de tipos. El archivo generado sigue diciendo import { v } from "@lib/util", así que otra cosa tiene que resolverlo en tiempo de ejecución: un bundler configurado con el mismo alias, o el campo imports de Node en package.json (alias que empiezan por #, como "#lib/*": "./dist/lib/*"), que funciona sin ninguna herramienta extra. baseUrl desaparece en TypeScript 7 (error TS5102); escribe las entradas de paths relativas al tsconfig.json con un ./ delante.

Preguntas frecuentes

¿Qué diferencia hay entre import e import type en TypeScript?

import type { User } from "./user.js" solo puede traer tipos, y la sentencia entera se elimina de la salida en JavaScript. Un import normal puede traer valores y tipos; TypeScript quita los nombres que solo se usan como tipos, pero con verbatimModuleSyntax te exige marcarlos con type para que la salida sea exactamente lo que escribiste.

¿Por qué TypeScript quiere .js en las rutas de import?

Con module: node16 o nodenext, un archivo ES module debe importar con el nombre real del archivo que Node cargará en tiempo de ejecución, y ese archivo es el .js compilado. TypeScript resuelve ./math.js a math.ts al comprobar los tipos. Omitir la extensión en un archivo ES module es el error TS2835.

¿Uso export default o exports con nombre en TypeScript?

Los dos funcionan. Muchos equipos prefieren los exports con nombre: el nombre es el mismo en todos los archivos que lo importan, los editores los autoimportan de forma fiable y renombrar es una refactorización y no una búsqueda. Un export por defecto deja que cada archivo que importa elija su propio nombre.

¿La opción paths de tsconfig cambia el import en la salida?

No. paths solo le dice al comprobador de tipos dónde encontrar un módulo. El JavaScript generado mantiene "@lib/util" tal cual, así que un bundler, o el campo imports de Node en package.json, tiene que resolverlo en tiempo de ejecución.

¿Cómo arreglo "Cannot find module" en TypeScript?

El error TS2307 significa que la ruta no lleva a un archivo que TypeScript pueda encontrar, o que un paquete no incluye tipos. Revisa la ruta relativa y la extensión, instala el paquete @types/... si el paquete no trae tipos propios, o escribe un pequeño archivo de declaraciones para él.

Coddy programming languages illustration

Aprende a programar con Coddy

COMENZAR