Sistema de módulos ES en TypeScript
En TypeScript, cualquier archivo que contenga al menos una declaración import o export en el nivel superior se considera un módulo. Los archivos sin estas declaraciones se tratan como scripts, cuyo contenido está disponible en el ámbito global.

// matematicas.ts - Este archivo ES un módulo (tiene export)
export function sumar(a: number, b: number): number {
return a + b
}
// Los valores no exportados son privados al módulo
const PI = 3.14159
export function areaCirculo(radio: number): number {
return PI * radio * radio
}
Los módulos tienen su propio ámbito léxico: las variables, funciones, clases y tipos declarados en un módulo no están disponibles fuera de el a menos que se exporten explícitamente.
Si un archivo no tiene import ni export pero se quiere tratar como módulo, se puede añadir
export {}al final. Esto convierte el archivo en un módulo sin exportar nada.
Módulos vs scripts
La diferencia fundamental entre módulos y scripts es el ámbito de las declaraciones:
// script1.ts (sin import/export, es un script)
const MAX = 100
function inicializar() { /* ... */ }
// script2.ts (otro script)
// const MAX = 200 // Error: identificador duplicado en ámbito global
// modulo1.ts
export const MAX = 100
export function inicializar() { /* ... */ }
// modulo2.ts
export const MAX = 200 // Sin error: ambitos separados
export function inicializar() { /* ... */ }
Exportaciones con nombre
Las exportaciones con nombre (named exports) permiten exponer múltiples valores desde un módulo. Cada valor exportado mantiene su nombre original:
// logger.ts
export const NIVELES = {
INFO: "info",
WARN: "warn",
ERROR: "error"
} as const
export interface MensajeLog {
nivel: string
mensaje: string
timestamp: Date
}
export class Logger {
registrar(nivel: string, mensaje: string): void {
console.log(`[${nivel.toUpperCase()}]: ${mensaje}`)
}
}
export function formatearMensaje(msg: MensajeLog): string {
return `[${msg.nivel}][${msg.timestamp.toISOString()}] ${msg.mensaje}`
}
Existe una sintaxis alternativa que agrupa las exportaciones al final del archivo:
// utilidades.ts
const VERSION = "1.0.0"
function formatearFecha(fecha: Date): string {
return fecha.toISOString().split("T")[0]
}
interface Resultado<T> {
datos: T
exitoso: boolean
}
export { VERSION, formatearFecha, Resultado }
Renombrar al exportar
Se pueden renombrar los elementos durante la exportación con la palabra clave as:
// config.ts
const claveApi = "abc123"
const urlApi = "https://api.ejemplo.com"
export {
claveApi as API_KEY,
urlApi as API_URL
}
Importaciones con nombre
Para consumir exportaciones con nombre se usa la sintaxis de desestructuración:
// app.ts
import { Logger, NIVELES, MensajeLog } from "./logger"
const logger = new Logger()
logger.registrar(NIVELES.INFO, "Aplicación iniciada")
const mensaje: MensajeLog = {
nivel: NIVELES.WARN,
mensaje: "Recurso no encontrado",
timestamp: new Date()
}
Importación selectiva
Se pueden importar solo los elementos necesarios, lo que facilita el tree-shaking en bundlers:
import { sumar } from "./matematicas"
console.log(sumar(10, 5))
Renombrar al importar
La palabra clave as permite renombrar elementos durante la importación para evitar colisiones:
import { Logger as ServicioLog } from "./logger"
import { Logger as LoggerArchivo } from "./logger-archivo"
const logConsola = new ServicioLog()
const logArchivo = new LoggerArchivo()
Importar todo el módulo
El operador * as importa todas las exportaciones como un objeto namespace:
import * as matematicas from "./matematicas"
console.log(matematicas.sumar(5, 3))
console.log(matematicas.areaCirculo(10))
Exportaciones por defecto
Cada módulo puede tener una única exportación por defecto. Se usa cuando el módulo tiene un propósito principal claro:
// cliente-api.ts
export default class ClienteAPI {
constructor(private urlBase: string) {}
async obtener<T>(endpoint: string): Promise<T> {
const respuesta = await fetch(`${this.urlBase}/${endpoint}`)
return respuesta.json()
}
async enviar<T>(endpoint: string, datos: unknown): Promise<T> {
const respuesta = await fetch(`${this.urlBase}/${endpoint}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(datos)
})
return respuesta.json()
}
}
Al importar, no se usan llaves y se puede asignar cualquier nombre:
import ClienteAPI from "./cliente-api"
// o: import API from "./cliente-api"
const api = new ClienteAPI("https://api.ejemplo.com")
Combinar exportación por defecto y con nombre
Un módulo puede tener ambos tipos de exportación simultáneamente:
// validación.ts
export function esEmail(valor: string): boolean {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(valor)
}
export function esNumero(valor: string): boolean {
return !isNaN(Number(valor))
}
export default class Validador {
validar(
esquema: Record<string, (valor: string) => boolean>,
datos: Record<string, string>
): boolean {
for (const [campo, validar] of Object.entries(esquema)) {
if (!validar(datos[campo])) return false
}
return true
}
}
import Validador, { esEmail, esNumero } from "./validación"
const validador = new Validador()
const resultado = validador.validar(
{ email: esEmail, edad: esNumero },
{ email: "ana@ejemplo.com", edad: "28" }
)
Importaciones de solo tipo
TypeScript permite importar exclusivamente tipos con la sintaxis import type. Estas importaciones se eliminan completamente del JavaScript generado:
// tipos.ts
export interface Usuario {
id: string
nombre: string
email: string
}
export type IdUsuario = string
export enum RolUsuario {
Admin = "admin",
Editor = "editor",
Lector = "lector"
}
// servicio-usuario.ts
import type { Usuario, IdUsuario } from "./tipos"
import { RolUsuario } from "./tipos"
// Usuario e IdUsuario solo se usan como tipos, no generan JavaScript
function obtenerUsuario(id: IdUsuario): Promise<Usuario> {
return fetch(`/api/usuarios/${id}`).then(r => r.json())
}
// RolUsuario se importa normalmente porque se usa como valor
const rolPorDefecto = RolUsuario.Lector
También se puede marcar cada importación individual con type:
import { type Usuario, type IdUsuario, RolUsuario } from "./tipos"
Las importaciones de solo tipo son esenciales cuando se usan transpiladores como Babel, swc o esbuild, que no ejecutan el compilador de TypeScript completo. Permiten a estos transpiladores saber que importaciones pueden eliminar de forma segura.
Export type
De forma analoga, se pueden reexportar solo tipos:
export type { Usuario, IdUsuario } from "./tipos"
Reexportaciones
Las reexportaciones permiten crear módulos intermediarios que agrupan exportaciones de otros módulos sin importarlas localmente:
// modelos/usuario.ts
export interface Usuario {
id: string
nombre: string
}
// modelos/producto.ts
export interface Producto {
id: string
titulo: string
precio: number
}
// modelos/index.ts - Reexportacion
export { Usuario } from "./usuario"
export { Producto } from "./producto"
Reexportar todo
El operador * reexporta todas las exportaciones con nombre:
// modelos/index.ts
export * from "./usuario"
export * from "./producto"
Reexportar exportación por defecto
La exportación por defecto se puede reexportar como exportación con nombre:
// componentes/index.ts
export { default as Boton } from "./Boton"
export { default as Input } from "./Input"
export { default as Selector } from "./Selector"
Renombrar al reexportar
// api/index.ts
export { validar as validarEmail } from "./validador-email"
export { validar as validarPassword } from "./validador-password"
Barrel files (index.ts)
El patrón barrel consiste en crear un archivo index.ts que reexporta los elementos de una carpeta, simplificando las importaciones:
src/
modelos/
usuario.ts
producto.ts
pedido.ts
index.ts <-- barrel file
servicios/
auth.ts
api.ts
index.ts <-- barrel file
// modelos/index.ts
export * from "./usuario"
export * from "./producto"
export * from "./pedido"
Los consumidores importan directamente desde la carpeta:
// Sin barrel file
import { Usuario } from "./modelos/usuario"
import { Producto } from "./modelos/producto"
import { Pedido } from "./modelos/pedido"
// Con barrel file
import { Usuario, Producto, Pedido } from "./modelos"
Fachada de API pública
Los barrel files permiten ocultar la estructura interna exponiendo solo una API pública:
// servicios/interno/usuarios.ts
export function obtenerTodos() { /* ... */ }
export function obtenerPorId(id: string) { /* ... */ }
export function _validacionInterna() { /* ... */ }
// servicios/index.ts - Solo expone la API pública
export { obtenerTodos, obtenerPorId } from "./interno/usuarios"
// No reexporta _validacionInterna
Consideraciones de rendimiento
Las reexportaciones con export * pueden dificultar el tree-shaking en algunos bundlers. Para módulos grandes, las reexportaciones selectivas son preferibles:
// Mejor para tree-shaking
export { funcion1, funcion2 } from "./módulo"
// Puede dificultar tree-shaking
export * from "./módulo"
Importaciones dinamicas
La función import() permite cargar módulos de forma asíncrona, útil para code splitting:
async function cargarModuloMatematicas() {
const matematicas = await import("./matematicas")
console.log(matematicas.sumar(5, 3))
}
// O con desestructuracion
async function procesarDatos() {
const { formatearFecha } = await import("./utilidades")
console.log(formatearFecha(new Date()))
}
TypeScript infiere correctamente los tipos de las importaciones dinamicas, proporcionando autocompletado y verificación de tipos incluso en módulos cargados de forma diferida.
Organización de módulos en proyectos
Una estructura recomendada para proyectos TypeScript:
src/
tipos/ # Tipos e interfaces compartidos
index.ts
modelos/ # Modelos de dominio
index.ts
servicios/ # Logica de negocio
index.ts
utilidades/ # Funciones auxiliares
index.ts
index.ts # Punto de entrada principal
Cada carpeta contiene su barrel file y expone una API pública limpia. Las dependencias entre módulos fluyen en una dirección clara, facilitando el mantenimiento y la comprensión del código.
// src/index.ts - Punto de entrada principal de una librería
export type { Usuario, Producto } from "./tipos"
export { ServicioUsuarios } from "./servicios"
export { formatearMoneda, formatearFecha } from "./utilidades"
Este enfoque garantiza que los consumidores del módulo tengan acceso a una API bien definida sin necesidad de conocer la estructura interna del proyecto.
Alan Sastre
Ingeniero de Software y formador, CEO en CertiDevs
Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, TypeScript es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.
Más tutoriales de TypeScript
Explora más contenido relacionado con TypeScript y continúa aprendiendo con nuestros tutoriales gratuitos.
Aprendizajes de esta lección
Comprender el sistema de módulos ES en TypeScript, dominar las exportaciones con nombre y por defecto, usar reexportaciones y barrel files, aplicar importaciones de solo tipo con import type, y organizar módulos en proyectos reales.