TypeScript

TypeScript

Tutorial TypeScript: Resolución de módulos

Aprende las estrategias de resolución de módulos en TypeScript y cómo configurar paths y alias para optimizar tus importaciones.

Aprende TypeScript y certifícate

Estrategias de resolución

La resolución de módulos es uno de los aspectos fundamentales para comprender cómo TypeScript localiza y carga los archivos que importamos en nuestro código. Cuando escribimos una sentencia import, el compilador necesita determinar exactamente a qué archivo se refiere esa importación, y para ello utiliza diferentes estrategias de resolución.

TypeScript ofrece varias estrategias para resolver las importaciones de módulos, cada una con sus propias reglas y casos de uso. Estas estrategias determinan cómo el compilador interpreta las rutas de importación y dónde busca los archivos correspondientes.

Estrategias básicas de resolución

TypeScript implementa dos estrategias principales de resolución de módulos:

  • Classic: La estrategia original y más simple.
  • Node: Emula el comportamiento de resolución de Node.js.

Podemos configurar la estrategia que queremos utilizar mediante la opción moduleResolution en el archivo tsconfig.json:

{
  "compilerOptions": {
    "moduleResolution": "node"
  }
}

Estrategia Classic

La estrategia Classic es la más simple y fue la implementación original en TypeScript. Funciona de la siguiente manera:

  • Importaciones relativas (que comienzan con ./ o ../):
  1. Busca el archivo directamente con la extensión .ts
  2. Busca el archivo como directorio que contiene un index.ts
  • Importaciones no relativas (que no comienzan con ./ o ../):
  1. Busca el módulo en el directorio actual
  2. Luego busca en directorios padres de forma recursiva
// Ejemplo de importación relativa
import { Usuario } from './modelos/usuario';
// Buscará: ./modelos/usuario.ts o ./modelos/usuario/index.ts

// Ejemplo de importación no relativa
import { formatearFecha } from 'utilidades';
// Buscará: ./utilidades.ts, ./utilidades/index.ts, ../utilidades.ts, etc.

Esta estrategia es simple pero limitada, especialmente para proyectos grandes o que utilizan paquetes de npm.

Estrategia Node

La estrategia Node es más compleja y emula el comportamiento del sistema de módulos de Node.js. Es la opción recomendada para la mayoría de los proyectos modernos. Funciona de la siguiente manera:

  • Importaciones relativas (que comienzan con ./ o ../):
  1. Busca el archivo con extensión .ts, .tsx, .d.ts
  2. Busca el archivo como directorio que contiene un package.json con campo types o main
  3. Busca el archivo como directorio que contiene un index.ts, index.tsx o index.d.ts
  • Importaciones no relativas (que no comienzan con ./ o ../):
  1. Busca en el directorio node_modules más cercano
  2. Si no lo encuentra, sube un nivel y busca en el siguiente node_modules
  3. Continúa subiendo hasta encontrar el módulo o llegar a la raíz del sistema de archivos
// Importación relativa
import { Producto } from './modelos/producto';
// Buscará: ./modelos/producto.ts, ./modelos/producto.tsx, ./modelos/producto.d.ts,
// ./modelos/producto/package.json, ./modelos/producto/index.ts, etc.

// Importación no relativa
import { format } from 'date-fns';
// Buscará: ./node_modules/date-fns, ../node_modules/date-fns, etc.

Estrategia NodeNext

A partir de TypeScript 4.7, se introdujo una nueva estrategia llamada NodeNext, que implementa la resolución de módulos según las especificaciones más recientes de Node.js:

{
  "compilerOptions": {
    "moduleResolution": "NodeNext"
  }
}

Esta estrategia añade soporte para:

  • Extensiones en importaciones: Permite (y requiere) especificar extensiones en las importaciones
  • Importaciones de paquetes: Soporte mejorado para el campo exports en package.json
  • Resolución de ESM vs CommonJS: Comportamiento diferente según el tipo de módulo
// Con NodeNext, las extensiones son necesarias para ESM
import { Usuario } from './modelos/usuario.js'; // Nota la extensión .js

// Importación de subpaths definidos en exports de package.json
import { Button } from 'mi-libreria/components';

Estrategia Bundler

TypeScript 5.0 introdujo la estrategia Bundler, diseñada para proyectos que utilizan empaquetadores como Webpack, Rollup o Parcel:

{
  "compilerOptions": {
    "moduleResolution": "Bundler"
  }
}

Esta estrategia:

  • No requiere extensiones en las importaciones (a diferencia de NodeNext)
  • Soporta el campo exports de package.json
  • Está optimizada para el flujo de trabajo con empaquetadores
// Con Bundler, no necesitas extensiones
import { Usuario } from './modelos/usuario';

// Pero sigue soportando subpaths de exports
import { Icon } from 'mi-ui-kit/icons';

Personalización de la resolución

Además de las estrategias predefinidas, TypeScript permite personalizar el proceso de resolución mediante varias opciones en tsconfig.json:

  • baseUrl: Define un directorio base para resolver módulos no relativos
{
  "compilerOptions": {
    "baseUrl": "./src"
  }
}

Con esta configuración, podemos importar desde la raíz del proyecto:

// Sin baseUrl
import { Usuario } from '../../modelos/usuario';

// Con baseUrl="./src"
import { Usuario } from 'modelos/usuario';
  • rootDirs: Permite tratar múltiples directorios como si fueran uno solo
{
  "compilerOptions": {
    "rootDirs": ["./src", "./generados"]
  }
}

Esto permite importaciones entre estos directorios como si estuvieran en la misma ubicación:

// Aunque los archivos estén en directorios diferentes físicamente,
// TypeScript los trata como si estuvieran en el mismo directorio
import { Componente } from './componente';

Resolución con alias

Una estrategia muy útil es definir alias para rutas de importación frecuentes. Esto se logra mediante la opción paths:

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@modelos/*": ["src/modelos/*"],
      "@utils/*": ["src/utilidades/*"],
      "@componentes/*": ["src/ui/componentes/*"]
    }
  }
}

Con esta configuración, podemos usar los alias en nuestras importaciones:

// Sin alias
import { Usuario } from '../../modelos/usuario';
import { formatearFecha } from '../../utilidades/fecha';

// Con alias
import { Usuario } from '@modelos/usuario';
import { formatearFecha } from '@utils/fecha';

Los alias ofrecen varias ventajas:

  • Importaciones más limpias y fáciles de leer
  • Independencia de la estructura de directorios: si movemos archivos, solo necesitamos actualizar la configuración de paths
  • Evita problemas con rutas relativas profundas (../../../)

Estrategias para bibliotecas externas

Cuando trabajamos con bibliotecas externas, TypeScript utiliza archivos de declaración (.d.ts) para proporcionar información de tipos. Existen varias estrategias para resolver estos tipos:

  • Tipos incluidos: Muchas bibliotecas incluyen sus propios archivos de declaración
// La biblioteca ya incluye tipos
import { useState } from 'react';
  • DefinitelyTyped: Para bibliotecas sin tipos, podemos instalar paquetes @types
npm install lodash
npm install @types/lodash --save-dev
// TypeScript usará automáticamente los tipos de @types/lodash
import _ from 'lodash';
  • Declaraciones manuales: Podemos crear nuestros propios archivos de declaración
// declarations.d.ts
declare module 'mi-libreria-sin-tipos' {
  export function funcionUtil(valor: string): number;
}

Estrategias para monorepos

En proyectos de monorepo (múltiples paquetes en un solo repositorio), existen estrategias específicas:

  • Project References: TypeScript permite definir referencias entre proyectos
// tsconfig.json del proyecto principal
{
  "references": [
    { "path": "./paquetes/utilidades" },
    { "path": "./paquetes/componentes" }
  ]
}
  • Workspaces: Integración con workspaces de npm o yarn
// package.json
{
  "workspaces": ["paquetes/*"]
}

Esto permite importaciones entre paquetes del monorepo:

// Importación desde otro paquete del monorepo
import { formatearMoneda } from '@mi-org/utilidades';

Depuración de problemas de resolución

Cuando enfrentamos problemas de resolución de módulos, podemos utilizar la opción --traceResolution para ver cómo TypeScript está intentando resolver las importaciones:

tsc --traceResolution

Esto mostrará información detallada sobre el proceso de resolución:

======== Resolving module 'lodash' from '/proyecto/src/app.ts'. ========
Module resolution kind is not specified, using 'NodeJs'.
Loading module 'lodash' from 'node_modules' folder.
File '/proyecto/src/node_modules/lodash.ts' does not exist.
File '/proyecto/src/node_modules/lodash.tsx' does not exist.
...

Esta información es invaluable para diagnosticar problemas de importación complejos.

Paths en tsconfig

La configuración de paths en el archivo tsconfig.json es una de las características más útiles de TypeScript para gestionar importaciones en proyectos de tamaño medio y grande. Esta funcionalidad permite crear alias de importación que simplifican significativamente la forma en que referenciamos módulos en nuestro código.

Fundamentos de paths

Los paths son mapeos personalizados que le indican al compilador de TypeScript cómo resolver nombres de módulos específicos. Funcionan como un sistema de alias que traduce patrones de importación a rutas de archivo reales en el sistema de archivos.

Para configurar paths, necesitamos definir dos propiedades en el archivo tsconfig.json:

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@app/*": ["src/app/*"],
      "@core/*": ["src/core/*"],
      "@shared/*": ["src/shared/*"]
    }
  }
}

En esta configuración:

  • baseUrl: Define el directorio base desde el cual se resolverán todas las rutas no relativas y los paths personalizados.
  • paths: Define un conjunto de mapeos entre patrones de nombres de módulos y sus ubicaciones reales.

Sintaxis y patrones

La sintaxis de paths sigue un formato específico:

"paths": {
  "patrón": ["ubicación1", "ubicación2", ...]
}

Donde:

  • patrón: Es el patrón de importación que queremos usar en nuestro código.
  • ubicaciones: Son las rutas reales donde TypeScript buscará los módulos (en orden).

Los patrones pueden incluir comodines (*) que capturan parte de la ruta:

"paths": {
  "@modelos/*": ["src/modelos/*"]
}

En este ejemplo, una importación como import { Usuario } from '@modelos/usuario' se traducirá a import { Usuario } from 'src/modelos/usuario'.

Casos de uso comunes

1. Simplificar importaciones profundas

Sin paths:

// Importación con rutas relativas profundas
import { validarEmail } from '../../../utils/validadores/email';
import { Usuario } from '../../../modelos/usuario/usuario.model';

Con paths:

// tsconfig.json
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@utils/*": ["src/utils/*"],
      "@modelos/*": ["src/modelos/*"]
    }
  }
}
// Importación con alias
import { validarEmail } from '@utils/validadores/email';
import { Usuario } from '@modelos/usuario/usuario.model';

2. Independencia de la estructura de directorios

Los paths permiten desacoplar el código de la estructura física de archivos. Si reorganizamos nuestro proyecto, solo necesitamos actualizar la configuración de paths, no todas las importaciones:

// Antes de la reorganización
{
  "paths": {
    "@servicios/*": ["src/servicios/*"]
  }
}

// Después de mover los servicios a otra ubicación
{
  "paths": {
    "@servicios/*": ["src/core/servicios/*"]
  }
}

El código que usa @servicios/... seguirá funcionando sin cambios.

3. Alias para bibliotecas externas

Podemos crear alias para bibliotecas externas o partes específicas de ellas:

{
  "paths": {
    "ui-components": ["node_modules/mi-libreria-ui/dist"],
    "ui-icons/*": ["node_modules/mi-libreria-ui/dist/icons/*"]
  }
}
// Importación simplificada
import { Button } from 'ui-components';
import { HomeIcon } from 'ui-icons/home';

4. Redirección para pruebas o entornos

Podemos usar paths para redirigir importaciones a versiones alternativas de módulos:

{
  "paths": {
    "@api/*": ["src/api/*"],
    // Para pruebas, redirigimos a implementaciones simuladas
    "@api/*": ["src/mocks/api/*"]
  }
}

Configuraciones avanzadas

Múltiples ubicaciones de búsqueda

Podemos especificar múltiples ubicaciones para un mismo patrón, y TypeScript las buscará en orden:

{
  "paths": {
    "@config/*": [
      "src/config/entorno/*",
      "src/config/base/*"
    ]
  }
}

TypeScript primero buscará en src/config/entorno/* y, si no encuentra el módulo, continuará con src/config/base/*.

Alias exactos (sin comodines)

Podemos definir alias para módulos específicos sin usar comodines:

{
  "paths": {
    "config": ["src/config/index.ts"],
    "logger": ["src/utils/logger.ts"]
  }
}
// Importación directa
import { configuracion } from 'config';
import { log } from 'logger';

Alias para directorios completos

También podemos crear alias para importar directorios completos:

{
  "paths": {
    "@componentes": ["src/ui/componentes/index.ts"]
  }
}
// Importa todo lo exportado desde el index.ts
import { Button, Card, Modal } from '@componentes';

Integración con empaquetadores

Para que los paths funcionen correctamente en tiempo de ejecución, necesitamos configurar nuestro empaquetador (Webpack, Rollup, etc.) para que reconozca estos alias:

Webpack

// webpack.config.js
const path = require('path');

module.exports = {
  // ...
  resolve: {
    alias: {
      '@app': path.resolve(__dirname, 'src/app'),
      '@core': path.resolve(__dirname, 'src/core'),
      '@shared': path.resolve(__dirname, 'src/shared')
    }
  }
};

Vite

// vite.config.js
import { defineConfig } from 'vite';
import path from 'path';

export default defineConfig({
  resolve: {
    alias: {
      '@app': path.resolve(__dirname, './src/app'),
      '@core': path.resolve(__dirname, './src/core'),
      '@shared': path.resolve(__dirname, './src/shared')
    }
  }
});

Consideraciones prácticas

Consistencia en el equipo

Es recomendable establecer convenciones claras para los alias en el equipo:

  • Usar prefijos consistentes (como @ para rutas internas)
  • Documentar el propósito de cada alias
  • Evitar crear demasiados alias que puedan confundir al equipo

Autocompletado y navegación

Una ventaja adicional de usar paths es que los editores modernos como VS Code proporcionan mejor autocompletado y navegación cuando están configurados correctamente:

import { } from '@utils/'; // El editor mostrará sugerencias de todos los módulos disponibles

Paths y outDir

Cuando usamos la opción outDir para especificar un directorio de salida, TypeScript no ajusta automáticamente las rutas de paths en el código compilado. Para solucionar esto, necesitamos usar un empaquetador o configurar tsconfig-paths en entornos Node.js:

npm install --save-dev tsconfig-paths
// En el punto de entrada de la aplicación
require('tsconfig-paths').register();

Depuración de problemas con paths

Si los paths no funcionan como esperamos, podemos usar el flag --traceResolution para ver cómo TypeScript está resolviendo las importaciones:

tsc --traceResolution

Esto mostrará información detallada sobre cómo se están resolviendo los módulos, incluyendo la aplicación de los paths configurados.

Patrones recomendados

  • Estructura por características: Organizar paths por características o dominios de la aplicación
{
  "paths": {
    "@usuarios/*": ["src/features/usuarios/*"],
    "@productos/*": ["src/features/productos/*"],
    "@pedidos/*": ["src/features/pedidos/*"]
  }
}
  • Capas arquitectónicas: Organizar paths por capas de la arquitectura
{
  "paths": {
    "@ui/*": ["src/presentation/*"],
    "@domain/*": ["src/domain/*"],
    "@data/*": ["src/data/*"],
    "@infra/*": ["src/infrastructure/*"]
  }
}
  • Barrels: Combinar paths con archivos "barrel" (index.ts que re-exporta)
// src/utils/index.ts (barrel file)
export * from './string-utils';
export * from './date-utils';
export * from './number-utils';
{
  "paths": {
    "@utils": ["src/utils/index.ts"]
  }
}
// Importación limpia de múltiples utilidades
import { formatDate, parseDate, formatCurrency } from '@utils';

Module resolution

El module resolution en TypeScript es el proceso mediante el cual el compilador determina a qué archivo corresponde cada declaración de importación. Este mecanismo es fundamental para entender cómo TypeScript encuentra y conecta los diferentes módulos de nuestra aplicación.

Algoritmos de resolución de módulos

TypeScript implementa diferentes algoritmos para resolver módulos, que podemos configurar en el archivo tsconfig.json mediante la opción moduleResolution. Los principales algoritmos son:

{
  "compilerOptions": {
    "moduleResolution": "node" // Opciones: "node", "classic", "bundler", "nodenext"
  }
}

Cada algoritmo sigue reglas específicas para localizar los módulos:

  • Classic: El algoritmo original de TypeScript, más simple pero menos potente
  • Node: Emula el comportamiento de Node.js (recomendado para la mayoría de proyectos)
  • NodeNext: Implementa las especificaciones más recientes de Node.js (ESM)
  • Bundler: Optimizado para trabajar con empaquetadores como Webpack o Vite

Resolución de importaciones relativas vs no relativas

TypeScript distingue entre dos tipos de importaciones:

  • Importaciones relativas: Comienzan con /, ./ o ../
  • Importaciones no relativas: No comienzan con estos prefijos
// Importación relativa
import { Usuario } from './models/usuario';

// Importación no relativa
import { formatDate } from 'date-utils';

El proceso de resolución es diferente para cada tipo:

Resolución de importaciones relativas

Para importaciones relativas, TypeScript busca el archivo directamente en la ubicación especificada:

import { Producto } from './models/producto';

Con el algoritmo node, TypeScript buscará en este orden:

  1. ./models/producto.ts
  2. ./models/producto.tsx
  3. ./models/producto.d.ts
  4. ./models/producto/index.ts
  5. ./models/producto/index.tsx
  6. ./models/producto/index.d.ts

Resolución de importaciones no relativas

Para importaciones no relativas, el proceso es más complejo:

import { Button } from 'ui-components';

Con el algoritmo node, TypeScript buscará:

  1. En el directorio node_modules más cercano
  2. Si no lo encuentra, sube un nivel y busca en el siguiente node_modules
  3. Continúa subiendo hasta encontrar el módulo o llegar a la raíz

Extensiones de archivo en importaciones

El manejo de extensiones varía según el algoritmo de resolución:

  • Con node y classic, las extensiones son opcionales:
// Válido con node/classic
import { Usuario } from './models/usuario';
  • Con nodenext para módulos ESM, las extensiones son obligatorias:
// Necesario con nodenext para ESM
import { Usuario } from './models/usuario.js';

Es importante notar que con nodenext, aunque escribimos .js en la importación, TypeScript sigue buscando el archivo .ts correspondiente durante la compilación.

Resolución con package.json

El archivo package.json juega un papel importante en la resolución de módulos, especialmente para paquetes publicados:

{
  "name": "mi-libreria",
  "main": "dist/index.js",      // Para CommonJS
  "module": "dist/index.mjs",   // Para ESM
  "types": "dist/index.d.ts",   // Declaraciones de tipos
  "exports": {
    ".": {
      "import": "./dist/index.mjs",
      "require": "./dist/index.js",
      "types": "./dist/index.d.ts"
    },
    "./utils": {
      "import": "./dist/utils/index.mjs",
      "require": "./dist/utils/index.js",
      "types": "./dist/utils/index.d.ts"
    }
  }
}

TypeScript utiliza estos campos para determinar qué archivos cargar:

  • types: Indica dónde encontrar las declaraciones de tipos
  • exports: Define puntos de entrada específicos y sus variantes
  • main/module: Utilizados como fallback si no hay exports

Resolución con el campo exports

El campo exports en package.json permite definir una API pública explícita para un paquete:

{
  "exports": {
    ".": "./dist/index.js",
    "./utils": "./dist/utils/index.js",
    "./components/*": "./dist/components/*.js"
  }
}

Esto permite importaciones como:

import { algo } from 'paquete';              // Usa "."
import { utilidad } from 'paquete/utils';    // Usa "./utils"
import { Button } from 'paquete/components/button'; // Usa "./components/*"

Las ventajas de usar exports incluyen:

  • Encapsulación: Solo se pueden importar los paths explícitamente definidos
  • Mapeo condicional: Diferentes archivos según el entorno o tipo de importación
  • Compatibilidad: Mejor soporte para ESM y CommonJS simultáneamente

Resolución con TypeScript + Node.js

Cuando trabajamos con Node.js, es importante entender cómo interactúan los sistemas de módulos:

// archivo.ts
import { algo } from './otro-archivo';
  1. TypeScript compila a JavaScript:
// archivo.js (compilado)
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const otro_archivo_1 = require("./otro-archivo");
  1. Node.js ejecuta el código compilado y resuelve ./otro-archivo según sus propias reglas

Para evitar problemas, debemos asegurarnos de que:

  • La configuración de module en tsconfig.json sea compatible con Node.js
  • Las extensiones y rutas sean correctas para el entorno de ejecución

Resolución con archivos de declaración

Los archivos de declaración (.d.ts) son fundamentales para proporcionar información de tipos:

// tipos.d.ts
declare module 'mi-libreria' {
  export function utilidad(valor: string): number;
}

TypeScript busca estos archivos en:

  1. El directorio actual y sus padres
  2. En node_modules/@types/[nombre-paquete]
  3. En las ubicaciones especificadas por la opción typeRoots
{
  "compilerOptions": {
    "typeRoots": ["./tipos-personalizados", "./node_modules/@types"]
  }
}

Optimización de la resolución de módulos

Para mejorar el rendimiento de la resolución de módulos, podemos:

  • Usar la opción --traceResolution para diagnosticar problemas:
tsc --traceResolution
  • Configurar moduleDetection para controlar cómo se detectan los módulos:
{
  "compilerOptions": {
    "moduleDetection": "auto" // "force" | "legacy" | "auto"
  }
}
  • Utilizar la opción moduleSuffixes para personalizar los sufijos de búsqueda:
{
  "compilerOptions": {
    "moduleSuffixes": [".ios", ".native", ""]
  }
}

Con esta configuración, una importación como import { Component } from './button' buscará:

  1. ./button.ios.ts
  2. ./button.native.ts
  3. ./button.ts

Resolución en proyectos monorepo

En proyectos monorepo con múltiples paquetes, la resolución de módulos requiere configuración adicional:

/proyecto
  /packages
    /core
      package.json
      tsconfig.json
    /ui
      package.json
      tsconfig.json
  tsconfig.base.json

Podemos usar project references para conectar los proyectos:

// packages/ui/tsconfig.json
{
  "extends": "../../tsconfig.base.json",
  "references": [
    { "path": "../core" }
  ]
}

Esto permite importaciones entre paquetes y una compilación más eficiente:

// En packages/ui/src/button.ts
import { Theme } from '@project/core';

Resolución con diferentes targets

La resolución de módulos puede variar según el target de compilación:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler"
  }
}

Es importante alinear estas opciones para evitar problemas:

  • Para aplicaciones web modernas: target: "ES2022", module: "ESNext", moduleResolution: "bundler"
  • Para Node.js: target: "ES2022", module: "NodeNext", moduleResolution: "NodeNext"
  • Para compatibilidad amplia: target: "ES6", module: "CommonJS", moduleResolution: "node"

Casos prácticos de resolución

Resolución con alias de paths

Cuando usamos paths en tsconfig.json, el proceso de resolución se modifica:

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@app/*": ["src/app/*"]
    }
  }
}

Con esta configuración, una importación como:

import { UserService } from '@app/services/user.service';

Se resuelve a:

src/app/services/user.service.ts

Resolución con barrel files

Los barrel files (archivos índice que re-exportan) afectan la resolución:

// src/models/index.ts
export * from './user.model';
export * from './product.model';

Esto permite importaciones simplificadas:

// Importación directa
import { User, Product } from './models';

// En lugar de:
import { User } from './models/user.model';
import { Product } from './models/product.model';

TypeScript resuelve esto buscando ./models/index.ts automáticamente.

Resolución con archivos JavaScript

TypeScript puede resolver y proporcionar tipos para archivos JavaScript:

// Importando desde un archivo .js
import { formatDate } from './utils.js';

Para esto, TypeScript utiliza:

  1. Archivos de declaración adyacentes (utils.d.ts)
  2. JSDoc en el archivo JavaScript para inferir tipos
  3. La opción allowJs para incluir archivos JS en la compilación
{
  "compilerOptions": {
    "allowJs": true,
    "checkJs": true
  }
}

Depuración de problemas comunes

Algunos problemas frecuentes de resolución de módulos incluyen:

  • Módulo no encontrado: Verificar rutas, mayúsculas/minúsculas y extensiones
  • Tipos no encontrados: Instalar paquetes @types correspondientes
  • Conflictos entre ESM y CommonJS: Alinear configuraciones de module y moduleResolution
  • Problemas con paths: Asegurarse de que el empaquetador también reconozca los alias

Para diagnosticar estos problemas, podemos usar:

# Ver el proceso de resolución
tsc --traceResolution

# Verificar la salida de la compilación
tsc --noEmit --pretty

La comprensión profunda del sistema de resolución de módulos de TypeScript es esencial para construir aplicaciones bien estructuradas y mantenibles, especialmente a medida que crecen en complejidad.

Declaraciones de ambiente

Las declaraciones de ambiente (ambient declarations) en TypeScript representan una poderosa herramienta para integrar código JavaScript existente o bibliotecas externas en nuestros proyectos con tipado estático. Estas declaraciones permiten describir la forma y estructura de código que existe en otro lugar, sin implementar realmente su funcionalidad.

Cuando trabajamos con bibliotecas JavaScript que no incluyen definiciones de tipos, o cuando necesitamos extender tipos existentes, las declaraciones de ambiente nos permiten proporcionar esa información de tipos al compilador de TypeScript.

Archivos de declaración (.d.ts)

Los archivos con extensión .d.ts son archivos especiales que contienen únicamente información de tipos, sin implementación. Estos archivos son la base de las declaraciones de ambiente en TypeScript.

// ejemplo.d.ts
declare function calcularImpuesto(monto: number): number;
declare const IVA_GENERAL: number;

Estos archivos tienen varias características importantes:

  • No contienen implementaciones, solo definiciones de tipos
  • No generan código JavaScript al compilar
  • Pueden ser consumidos por el compilador de TypeScript para proporcionar información de tipos

La palabra clave declare

La palabra clave declare es fundamental en las declaraciones de ambiente. Le indica al compilador que el elemento existe en tiempo de ejecución, pero su implementación se proporciona en otro lugar.

// Declaración de una variable global
declare const API_URL: string;

// Declaración de una función global
declare function formatearMoneda(valor: number, moneda?: string): string;

// Declaración de una clase global
declare class HttpClient {
  get(url: string): Promise<any>;
  post(url: string, data: any): Promise<any>;
}

Cuando usamos declare, estamos diciendo: "confía en mí, esta variable/función/clase existe en tiempo de ejecución, aunque no puedas ver su implementación aquí".

Declaración de módulos

Una de las aplicaciones más comunes de las declaraciones de ambiente es definir tipos para módulos externos. Podemos declarar módulos de dos formas principales:

1. Declaración de módulos con nombre

// tipos-externos.d.ts
declare module 'biblioteca-sin-tipos' {
  export function metodoUtil(valor: string): number;
  export class ComponenteExterno {
    render(): void;
    setProps(props: any): void;
  }
  export const VERSION: string;
}

Con esta declaración, podemos importar y usar la biblioteca con tipado completo:

import { metodoUtil, ComponenteExterno } from 'biblioteca-sin-tipos';

const resultado = metodoUtil('test'); // TypeScript sabe que resultado es number
const componente = new ComponenteExterno();
componente.render();

2. Declaración de módulos comodín

También podemos crear declaraciones para grupos de módulos usando patrones comodín:

// modulos-json.d.ts
declare module '*.json' {
  const contenido: any;
  export default contenido;
}

// modulos-css.d.ts
declare module '*.css' {
  const clases: { [key: string]: string };
  export default clases;
}

Esto permite importar archivos que normalmente TypeScript no entendería:

import datos from './datos.json';
import estilos from './componente.css';

console.log(datos.propiedad);
const elemento = document.createElement('div');
elemento.className = estilos.contenedor;

Declaraciones globales

Podemos declarar tipos que estarán disponibles globalmente en toda nuestra aplicación sin necesidad de importarlos:

// globals.d.ts
declare global {
  interface Window {
    analytics: {
      trackEvent(evento: string, propiedades?: Record<string, any>): void;
      identificarUsuario(id: string): void;
    };
  }

  interface Array<T> {
    primerElemento(): T | undefined;
    ultimoElemento(): T | undefined;
  }
}

// Necesario para que el archivo sea un módulo
export {};

Estas declaraciones extienden interfaces existentes como Window o Array, añadiendo nuevas propiedades o métodos que podemos usar en toda nuestra aplicación:

// Uso de la extensión de Window
window.analytics.trackEvent('click_boton', { id: 'boton-comprar' });

// Uso de la extensión de Array
const numeros = [1, 2, 3, 4];
const primero = numeros.primerElemento(); // TypeScript entiende que es number | undefined

Declaración de espacios de nombres

Podemos usar declare namespace para definir grupos de tipos relacionados:

// api-tipos.d.ts
declare namespace API {
  interface Usuario {
    id: number;
    nombre: string;
    email: string;
  }

  interface Producto {
    id: number;
    nombre: string;
    precio: number;
  }

  interface Respuesta<T> {
    datos: T;
    estado: number;
    mensaje: string;
  }
}

Estos tipos se pueden usar directamente sin importación:

function obtenerUsuario(id: number): Promise<API.Respuesta<API.Usuario>> {
  // Implementación...
}

async function mostrarDatosUsuario(id: number) {
  const respuesta = await obtenerUsuario(id);
  const usuario: API.Usuario = respuesta.datos;
  console.log(usuario.nombre);
}

Fusión de declaraciones

Una característica poderosa de TypeScript es la capacidad de fusionar declaraciones con el mismo nombre. Esto nos permite extender tipos existentes:

// Biblioteca original
declare module 'mi-libreria' {
  export function metodo1(): void;
}

// Extensión en otro archivo
declare module 'mi-libreria' {
  export function metodo2(): void;
}

Después de la fusión, TypeScript reconocerá ambos métodos:

import { metodo1, metodo2 } from 'mi-libreria';

metodo1();
metodo2();

Esta característica es especialmente útil para extender bibliotecas de terceros con nuevas funcionalidades.

Importación de tipos en declaraciones

Podemos importar tipos en nuestros archivos de declaración para reutilizar definiciones:

// tipos-base.d.ts
declare module 'tipos-base' {
  export interface EntidadBase {
    id: number;
    createdAt: Date;
    updatedAt: Date;
  }
}

// modelos.d.ts
/// <reference path="./tipos-base.d.ts" />
declare module 'modelos' {
  import { EntidadBase } from 'tipos-base';
  
  export interface Usuario extends EntidadBase {
    nombre: string;
    email: string;
  }
}

La directiva /// <reference path="..." /> le indica a TypeScript que debe incluir otro archivo de declaración.

Declaraciones para APIs del navegador

Podemos extender las APIs del navegador que no están completamente tipadas:

// apis-navegador.d.ts
interface Navigator {
  // Añadir soporte para la API de compartir
  share?(data: {
    title?: string;
    text?: string;
    url?: string;
  }): Promise<void>;
  
  // Añadir soporte para la API de conexión
  connection?: {
    type: 'wifi' | 'cellular' | 'bluetooth' | 'ethernet' | 'none' | 'other' | 'unknown';
    addEventListener(event: string, listener: Function): void;
    removeEventListener(event: string, listener: Function): void;
  };
}

Ahora podemos usar estas APIs con seguridad de tipos:

if (navigator.share) {
  navigator.share({
    title: 'Artículo interesante',
    text: 'Mira este artículo que encontré',
    url: 'https://ejemplo.com/articulo'
  });
}

if (navigator.connection && navigator.connection.type === 'wifi') {
  console.log('Usuario conectado por WiFi');
}

Declaraciones para bibliotecas JavaScript

Cuando trabajamos con una biblioteca JavaScript sin tipos, podemos crear nuestras propias declaraciones:

// chart-library.d.ts
declare module 'chart-library' {
  export interface ChartOptions {
    width?: number;
    height?: number;
    colors?: string[];
    animate?: boolean;
  }
  
  export interface DataPoint {
    label: string;
    value: number;
  }
  
  export class Chart {
    constructor(element: HTMLElement, options?: ChartOptions);
    setData(data: DataPoint[]): void;
    render(): void;
    destroy(): void;
  }
  
  export function createChart(element: HTMLElement, options?: ChartOptions): Chart;
}

Ahora podemos usar la biblioteca con tipado completo:

import { Chart, DataPoint } from 'chart-library';

const datos: DataPoint[] = [
  { label: 'Enero', value: 100 },
  { label: 'Febrero', value: 150 },
  { label: 'Marzo', value: 200 }
];

const grafico = new Chart(document.getElementById('grafico')!, {
  width: 500,
  height: 300,
  animate: true
});

grafico.setData(datos);
grafico.render();

Declaraciones para archivos no JavaScript

TypeScript nos permite declarar tipos para importar archivos que no son JavaScript:

// archivos-especiales.d.ts
declare module '*.svg' {
  const content: string;
  export default content;
}

declare module '*.png' {
  const content: string;
  export default content;
}

declare module '*.csv' {
  const content: string[][];
  export default content;
}

Esto nos permite importar estos archivos en nuestro código TypeScript:

import logoSVG from './assets/logo.svg';
import iconoPNG from './assets/icono.png';
import datosCsv from './datos/ventas.csv';

const logo = document.createElement('img');
logo.src = logoSVG;

// Procesar datos CSV
datosCsv.forEach(fila => {
  console.log(`Producto: ${fila[0]}, Ventas: ${fila[1]}`);
});

Uso de @types

La comunidad de TypeScript mantiene el repositorio DefinitelyTyped, que proporciona tipos para miles de bibliotecas JavaScript a través de paquetes @types:

npm install lodash
npm install @types/lodash --save-dev

Después de instalar el paquete de tipos, podemos usar la biblioteca con tipado completo:

import _ from 'lodash';

const usuarios = [
  { id: 1, nombre: 'Ana' },
  { id: 2, nombre: 'Carlos' }
];

const usuario = _.find(usuarios, { id: 2 }); // TypeScript sabe que usuario es { id: number, nombre: string } | undefined

Creación de declaraciones personalizadas

Cuando necesitamos crear nuestras propias declaraciones, podemos seguir estos pasos:

  1. Crear un archivo .d.ts en nuestro proyecto
  2. Definir los tipos necesarios usando declare
  3. Asegurarnos de que TypeScript lo incluya en la compilación

Para incluir nuestros archivos de declaración, podemos:

  • Colocarlos en el directorio raíz del proyecto
  • Incluirlos en la propiedad include de tsconfig.json
  • Referenciarlos con /// <reference path="..." />
// tsconfig.json
{
  "compilerOptions": {
    // ...
  },
  "include": [
    "src/**/*.ts",
    "src/**/*.d.ts",
    "tipos/**/*.d.ts"
  ]
}

Depuración de declaraciones de ambiente

Cuando trabajamos con declaraciones de ambiente, pueden surgir problemas. Algunas técnicas para depurarlos:

  • Usar la opción --traceResolution para ver cómo TypeScript resuelve los módulos:
tsc --traceResolution
  • Verificar conflictos de tipos con la opción --diagnostics:
tsc --diagnostics
  • Comprobar si hay múltiples versiones de la misma declaración:
npm ls @types/biblioteca

Mejores prácticas

Al trabajar con declaraciones de ambiente, es recomendable seguir estas prácticas:

  • Mantener las declaraciones cerca del código: Coloca los archivos .d.ts junto a los archivos que los utilizan
  • Evitar any: Aunque es tentador usar any para ahorrar tiempo, intenta proporcionar tipos más específicos
  • Documentar las declaraciones: Añade comentarios JSDoc para mejorar la experiencia del desarrollador
  • Contribuir a DefinitelyTyped: Si creas declaraciones para una biblioteca pública, considera contribuirlas al repositorio DefinitelyTyped
// Ejemplo con JSDoc
declare module 'mi-libreria' {
  /**
   * Formatea un número como moneda
   * @param valor - El valor numérico a formatear
   * @param moneda - El código de moneda (por defecto: EUR)
   * @param locale - El locale a utilizar (por defecto: es-ES)
   * @returns El valor formateado como string
   */
  export function formatearMoneda(
    valor: number, 
    moneda?: string, 
    locale?: string
  ): string;
}

Las declaraciones de ambiente son una herramienta esencial en el ecosistema TypeScript, permitiéndonos integrar código JavaScript existente y bibliotecas externas con toda la potencia del sistema de tipos de TypeScript.

Aprende TypeScript online

Otros ejercicios de programación de TypeScript

Evalúa tus conocimientos de esta lección Resolución de módulos con nuestros retos de programación de tipo Test, Puzzle, Código y Proyecto con VSCode, guiados por IA.

Funciones

TypeScript
Test

Reto composición de funciones

TypeScript
Código

Reto tipos especiales

TypeScript
Código

Reto tipos genéricos

TypeScript
Código

Módulos

TypeScript
Test

Polimorfismo

TypeScript
Código

Funciones TypeScript

TypeScript
Código

Interfaces

TypeScript
Puzzle

Funciones puras

TypeScript
Puzzle

Reto namespaces

TypeScript
Código

Funciones flecha

TypeScript
Puzzle

Polimorfismo

TypeScript
Test

Operadores

TypeScript
Test

Conversor de unidades

TypeScript
Proyecto

Funciones flecha

TypeScript
Test

Control de flujo

TypeScript
Código

Herencia

TypeScript
Puzzle

Clases

TypeScript
Puzzle

Proyecto validación de tipado

TypeScript
Proyecto

Clases y objetos

TypeScript
Código

Encapsulación

TypeScript
Test

Herencia

TypeScript
Test

Proyecto sistema de votación

TypeScript
Proyecto

Reto genéricos con clases

TypeScript
Código

Inmutabilidad

TypeScript
Puzzle

Interfaces

TypeScript
Test

Funciones de alto orden

TypeScript
Test

Reto map y filter

TypeScript
Código

Control de flujo

TypeScript
Test

Interfaces

TypeScript
Código

Reto funciones orden superior

TypeScript
Código

Herencia y clases abstractas

TypeScript
Código

Reto tipos mapped

TypeScript
Código

Herencia de clases

TypeScript
Código

Reto funciones puras

TypeScript
Código

Variables y constantes

TypeScript
Puzzle

Introducción a TypeScript

TypeScript
Test

Reto testing unitario

TypeScript
Código

Funciones de primera clase

TypeScript
Puzzle

Clases

TypeScript
Test

OOP y CRUD en TypeScript

TypeScript
Proyecto

Interfaces y su implementación

TypeScript
Código

Tipos genéricos

TypeScript
Test

Namespaces

TypeScript
Test

Operadores y expresiones

TypeScript
Código

Proyecto generador de contraseñas

TypeScript
Proyecto

Reto unión e intersección

TypeScript
Código

Encapsulación

TypeScript
Puzzle

Tipos de unión e intersección

TypeScript
Test

Tipos de unión e intersección

TypeScript
Puzzle

Reto hola mundo en TS

TypeScript
Código

Variables y constantes

TypeScript
Código

Funciones puras

TypeScript
Test

Control de flujo

TypeScript
Código

Introducción a TypeScript

TypeScript
Código

Resolución de módulos

TypeScript
Test

Control de flujo

TypeScript
Puzzle

Reto tipos de utilidad

TypeScript
Código

Reto tipos literales y condicionales

TypeScript
Código

Reto exportar e importar

TypeScript
Código

Propiedades y métodos

TypeScript
Código

Tipos de utilidad

TypeScript
Test

Clases y objetos

TypeScript
Código

Tipos de datos, variables y constantes

TypeScript
Código

Proyecto Minigestor de tareas

TypeScript
Proyecto

Operadores

TypeScript
Puzzle

Funciones flecha y contexto

TypeScript
Código

Proyecto Inventario de productos

TypeScript
Proyecto

Funciones

TypeScript
Puzzle

Reto type aliases

TypeScript
Código

Funciones de alto orden

TypeScript
Puzzle

Funciones y parámetros tipados

TypeScript
Código

Tipos literales

TypeScript
Puzzle

Reto enums

TypeScript
Código

Tipos de utilidad

TypeScript
Puzzle

Modificadores de acceso y encapsulación

TypeScript
Código

Polimorfismo

TypeScript
Puzzle

Tipos genéricos

TypeScript
Puzzle

Reto módulos

TypeScript
Código

Tipos literales

TypeScript
Test

Inmutabilidad

TypeScript
Test

Proyecto Generator de datos

TypeScript
Proyecto

Variables y constantes

TypeScript
Test

Funciones de primera clase

TypeScript
Test

Todas las lecciones de TypeScript

Accede a todas las lecciones de TypeScript y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.

Introducción A Typescript

TypeScript

Introducción Y Entorno

Instalación Y Configuración De Typescript

TypeScript

Introducción Y Entorno

Tipos De Datos, Variables Y Constantes

TypeScript

Sintaxis

Operadores Y Expresiones

TypeScript

Sintaxis

Control De Flujo

TypeScript

Sintaxis

Funciones Y Parámetros Tipados

TypeScript

Sintaxis

Funciones Flecha Y Contexto

TypeScript

Sintaxis

Enums

TypeScript

Sintaxis

Type Aliases Y Aserciones De Tipo

TypeScript

Sintaxis

Clases Y Objetos

TypeScript

Programación Orientada A Objetos

Interfaces Y Su Implementación

TypeScript

Programación Orientada A Objetos

Modificadores De Acceso Y Encapsulación

TypeScript

Programación Orientada A Objetos

Herencia Y Clases Abstractas

TypeScript

Programación Orientada A Objetos

Polimorfismo

TypeScript

Programación Orientada A Objetos

Decoradores Básicos

TypeScript

Programación Orientada A Objetos

Propiedades Y Métodos

TypeScript

Programación Orientada A Objetos

Inmutabilidad

TypeScript

Programación Funcional

Funciones Puras Y Efectos Secundarios

TypeScript

Programación Funcional

Funciones De Primera Clase

TypeScript

Programación Funcional

Funciones De Alto Orden

TypeScript

Programación Funcional

Conceptos Básicos E Inmutabilidad

TypeScript

Programación Funcional

Funciones De Primera Clase Y Orden Superior

TypeScript

Programación Funcional

Composición De Funciones

TypeScript

Programación Funcional

Métodos Funcionales De Arrays (Map, Filter, Reduce)

TypeScript

Programación Funcional

Tipos Literales Y Tipos Condicionales

TypeScript

Tipos Intermedios Y Avanzados

Tipos Genéricos Básicos

TypeScript

Tipos Intermedios Y Avanzados

Tipos De Unión E Intersección

TypeScript

Tipos Intermedios Y Avanzados

Tipos De Utilidad (Partial, Required, Pick, Etc)

TypeScript

Tipos Intermedios Y Avanzados

Unknown, Never Y Tipos Especiales

TypeScript

Tipos Intermedios Y Avanzados

Tipos Mapped

TypeScript

Tipos Intermedios Y Avanzados

Genéricos Con Clases E Interfaces

TypeScript

Tipos Intermedios Y Avanzados

Módulos

TypeScript

Namespaces Y Módulos

Namespaces

TypeScript

Namespaces Y Módulos

Resolución De Módulos

TypeScript

Namespaces Y Módulos

Exportación E Importación De Módulos

TypeScript

Namespaces Y Módulos

Introducción A Módulos

TypeScript

Namespaces Y Módulos

Testing Unitario En Typescript

TypeScript

Testing

Accede GRATIS a TypeScript y certifícate

En esta lección

Objetivos de aprendizaje de esta lección

  • Comprender las diferentes estrategias de resolución de módulos en TypeScript (Classic, Node, NodeNext, Bundler).
  • Aprender a configurar y utilizar alias y paths en el archivo tsconfig.json para simplificar importaciones.
  • Entender el proceso de resolución de importaciones relativas y no relativas, incluyendo el manejo de extensiones y archivos de declaración.
  • Conocer cómo TypeScript interactúa con bibliotecas externas y monorepos en la resolución de módulos.
  • Saber cómo depurar problemas comunes relacionados con la resolución de módulos y paths en TypeScript.