En esta página

Módulos y namespaces

10 min lectura TextoCap. 5 — TypeScript en práctica

Módulos ES en TypeScript

TypeScript trabaja con el sistema de módulos ES (import/export) de forma nativa. Cada archivo con al menos un import o export de nivel superior es un módulo; el resto son scripts globales (algo que debes evitar en proyectos modernos).


Exportaciones e importaciones básicas

// src/matematicas.ts
export function sumar(a: number, b: number): number {
  return a + b;
}

export function multiplicar(a: number, b: number): number {
  return a * b;
}

export const PI = 3.14159265358979;

// Exportación por defecto (úsala con moderación)
export default function restar(a: number, b: number): number {
  return a - b;
}
// src/main.ts
import restar, { sumar, multiplicar, PI } from "./matematicas";
// O renombrando:
import { sumar as add } from "./matematicas";
// O importando todo el namespace:
import * as Mat from "./matematicas";

console.log(sumar(2, 3));          // 5
console.log(Mat.multiplicar(4, 5)); // 20

import type: importaciones de solo tipo

Cuando solo necesitas un tipo (interfaz, type alias, enum de tipos), usa import type. TypeScript garantiza que esa importación se elimina completamente en el JavaScript emitido:

// Importación mixta: valor y tipo
import { ServicioUsuarios, type Usuario } from "./servicios/usuarios";

// Importación solo de tipos
import type { Tarea, EstadoTarea, CrearTareaDto } from "./modelos/tarea";

// En archivos .d.ts siempre usa import type para referencias a tipos de otras librerías
import type { Request, Response } from "express";

También puedes usar export type para re-exportar solo tipos:

// Correcto: re-exportar un tipo desde un barrel
export type { Usuario, Perfil } from "./modelos";

// Incorrecto en archivos que serán importados sin bundler:
// export { Usuario } from "./modelos"; // puede generar código de runtime si no hay tree-shaking

Resolución de módulos: node16 y bundler

La configuración moduleResolution en tsconfig.json determina cómo TypeScript busca los archivos importados:

{
  "compilerOptions": {
    "module": "ESNext",
    "moduleResolution": "bundler"
  }
}
Modo Cuándo usarlo
bundler Proyectos con Vite, Webpack, esbuild, Rollup
node16 / nodenext APIs de Node.js con ESM nativo
node (legacy) CommonJS; evitar en proyectos nuevos

Con moduleResolution: "node16", las importaciones de archivos .ts deben incluir la extensión .js (la que tendrá el archivo compilado):

// Con moduleResolution: "node16"
import { calcular } from "./calculo.js"; // ← extensión .js aunque el fuente sea .ts

Con "bundler", el bundler resuelve extensiones automáticamente y no necesitas incluirlas.


Path aliases

Los path aliases evitan las rutas relativas largas como ../../../servicios/usuarios. Se configuran en tsconfig.json y en la configuración del bundler:

// tsconfig.json
{
  "compilerOptions": {
    "baseUrl": "src",
    "paths": {
      "@core/*":      ["core/*"],
      "@features/*":  ["features/*"],
      "@shared/*":    ["shared/*"],
      "@models/*":    ["models/*"],
      "@utils/*":     ["utils/*"]
    }
  }
}
// Antes (rutas relativas):
import { ServicioAuth } from "../../../core/auth/servicio-auth";
import type { Usuario }  from "../../models/usuario";

// Después (con aliases):
import { ServicioAuth } from "@core/auth/servicio-auth";
import type { Usuario }  from "@models/usuario";

Nota: si usas Vite, también debes configurar resolve.alias en vite.config.ts. Si usas ts-node, necesitas tsconfig-paths.


Barrel files (index.ts)

Un barrel file (archivo barril) re-exporta múltiples módulos desde un punto de entrada único. Facilita importaciones limpias sin exponer la estructura interna de un módulo:

src/
  features/
    usuarios/
      repositorio-usuarios.ts
      servicio-usuarios.ts
      tipos-usuario.ts
      index.ts          ← barrel
    tareas/
      repositorio-tareas.ts
      servicio-tareas.ts
      tipos-tarea.ts
      index.ts          ← barrel
// src/features/usuarios/index.ts
export { RepositorioUsuarios }           from "./repositorio-usuarios";
export { ServicioUsuarios }              from "./servicio-usuarios";
export type { Usuario, CrearUsuarioDto } from "./tipos-usuario";
// Consumidor — importación limpia y sin rutas internas expuestas
import { ServicioUsuarios } from "@features/usuarios";
import type { Usuario }     from "@features/usuarios";

Namespaces (legacy)

Los namespaces son una característica propia de TypeScript para agrupar tipos y valores bajo un mismo nombre. En proyectos modernos con ES modules, los namespaces son prácticamente innecesarios, pero los encontrarás en código legado y en archivos .d.ts:

namespace Validacion {
  export interface Regla {
    mensaje: string;
    validar(valor: string): boolean;
  }

  export function requerido(nombre: string): Regla {
    return {
      mensaje: `${nombre} es obligatorio`,
      validar: (v) => v.trim().length > 0,
    };
  }

  export function longitudMinima(min: number): Regla {
    return {
      mensaje: `Mínimo ${min} caracteres`,
      validar: (v) => v.length >= min,
    };
  }
}

const reglas: Validacion.Regla[] = [
  Validacion.requerido("Nombre"),
  Validacion.longitudMinima(3),
];

Para código nuevo, prefiere módulos ES con exportaciones nombradas: son más estándar, tienen mejor soporte en herramientas y permiten tree-shaking.


Archivos de declaración (.d.ts)

Los archivos .d.ts contienen solo declaraciones de tipos, sin código JavaScript. Son la forma en que TypeScript conoce los tipos de librerías escritas en JavaScript puro:

// types/mi-libreria.d.ts
declare module "mi-libreria-sin-tipos" {
  export interface Opciones {
    url: string;
    timeout?: number;
    reintentos?: number;
  }

  export function peticion(opciones: Opciones): Promise<unknown>;
  export function cancelar(id: string): void;
  export const version: string;
}

Para extender tipos existentes de otras librerías (module augmentation):

// types/express-extension.d.ts
import type { Usuario } from "@models/usuario";

declare global {
  namespace Express {
    interface Request {
      usuario?: Usuario;
    }
  }
}

Módulos ambientes con `declare module`

Si instalas una librería que no tiene tipos ni en el paquete ni en @types, puedes crear un módulo ambiente para silenciar el error y añadir tipos básicos:

// types/legacy-util.d.ts
declare module "legacy-util" {
  export function formatear(valor: string, opciones?: { mayusculas?: boolean }): string;
  export function limpiar(valor: string): string;
}

Y en tsconfig.json:

{
  "compilerOptions": {
    "typeRoots": ["./node_modules/@types", "./types"]
  }
}

Ejemplo completo: proyecto con barrel exports y path aliases

// ─────────────────────────────────────────
// src/models/tipos-comunes.ts
// ─────────────────────────────────────────
export type ID = string;

export interface EntidadBase {
  id: ID;
  creadaEn: Date;
  actualizadaEn: Date;
}

// ─────────────────────────────────────────
// src/models/usuario.ts
// ─────────────────────────────────────────
import type { EntidadBase } from "./tipos-comunes";

export interface Usuario extends EntidadBase {
  nombre: string;
  email: string;
  rol: "admin" | "editor" | "lector";
}

export type CrearUsuarioDto = Omit<Usuario, keyof EntidadBase>;

// ─────────────────────────────────────────
// src/models/index.ts (barrel de models)
// ─────────────────────────────────────────
export type { ID, EntidadBase }       from "./tipos-comunes";
export type { Usuario, CrearUsuarioDto } from "./usuario";

// ─────────────────────────────────────────
// src/core/logger.ts
// ─────────────────────────────────────────
export const logger = {
  info: (msg: string, ...args: unknown[]) =>
    console.log(`[INFO] ${msg}`, ...args),
  error: (msg: string, ...args: unknown[]) =>
    console.error(`[ERROR] ${msg}`, ...args),
  warn: (msg: string, ...args: unknown[]) =>
    console.warn(`[WARN] ${msg}`, ...args),
};

// ─────────────────────────────────────────
// src/features/usuarios/servicio-usuarios.ts
// (usa path alias @models y @core)
// ─────────────────────────────────────────
import type { Usuario, CrearUsuarioDto } from "@models";
import { logger }                        from "@core/logger";

export class ServicioUsuarios {
  private usuarios: Map<string, Usuario> = new Map();

  crear(dto: CrearUsuarioDto): Usuario {
    const usuario: Usuario = {
      ...dto,
      id: crypto.randomUUID(),
      creadaEn: new Date(),
      actualizadaEn: new Date(),
    };
    this.usuarios.set(usuario.id, usuario);
    logger.info("Usuario creado:", usuario.nombre);
    return usuario;
  }

  obtener(id: string): Usuario | null {
    return this.usuarios.get(id) ?? null;
  }

  listar(): Usuario[] {
    return Array.from(this.usuarios.values());
  }
}

// ─────────────────────────────────────────
// src/features/usuarios/index.ts (barrel)
// ─────────────────────────────────────────
export { ServicioUsuarios }              from "./servicio-usuarios";
export type { Usuario, CrearUsuarioDto } from "@models";

// ─────────────────────────────────────────
// src/app.ts — punto de entrada limpio
// ─────────────────────────────────────────
import { ServicioUsuarios }  from "@features/usuarios";
import type { Usuario }      from "@features/usuarios";

const svc = new ServicioUsuarios();

const ana: Usuario = svc.crear({
  nombre: "Ana López",
  email: "[email protected]",
  rol: "admin",
});

const luis: Usuario = svc.crear({
  nombre: "Luis Mendoza",
  email: "[email protected]",
  rol: "editor",
});

console.log("Usuarios:", svc.listar().map((u) => u.nombre));
// ["Ana López", "Luis Mendoza"]

Resumen

Concepto Cuándo usarlo
export / import ES Siempre en código TypeScript moderno
import type Cuando solo necesitas tipos, para reducir bundle
moduleResolution: bundler Con Vite, Webpack, esbuild
Path aliases Proyectos con más de 2-3 niveles de carpetas
Barrel files Para exponer APIs públicas de cada feature
Namespaces Solo en archivos .d.ts o código legado
Archivos .d.ts Tipar librerías JS sin tipos propios

Con estos patrones tu base de código será escalable, sus importaciones serán legibles y el refactoring de rutas de archivos dejará de ser un problema. En la siguiente lección aprenderás a migrar un proyecto de JavaScript a TypeScript de forma gradual y sin romper nada.

Usa import type para reducir el bundle
Con `import type`, TypeScript garantiza que esos imports se eliminan completamente del JavaScript emitido. Es especialmente importante en proyectos que no usan bundler o en archivos .d.ts donde cualquier import de valor generaría código innecesario.
Cuidado con los barrel files en proyectos grandes
Los barrel files (index.ts) son convenientes, pero pueden crear ciclos de dependencia y aumentar el tiempo de inicio si re-exportan demasiado. En proyectos grandes, limita los barrels a los límites de feature/módulo y no re-exportes todo de forma indiscriminada.