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ícateEstrategias 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../
):
- Busca el archivo directamente con la extensión
.ts
- Busca el archivo como directorio que contiene un
index.ts
- Importaciones no relativas (que no comienzan con
./
o../
):
- Busca el módulo en el directorio actual
- 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../
):
- Busca el archivo con extensión
.ts
,.tsx
,.d.ts
- Busca el archivo como directorio que contiene un
package.json
con campotypes
omain
- Busca el archivo como directorio que contiene un
index.ts
,index.tsx
oindex.d.ts
- Importaciones no relativas (que no comienzan con
./
o../
):
- Busca en el directorio
node_modules
más cercano - Si no lo encuentra, sube un nivel y busca en el siguiente
node_modules
- 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
enpackage.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
depackage.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:
./models/producto.ts
./models/producto.tsx
./models/producto.d.ts
./models/producto/index.ts
./models/producto/index.tsx
./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á:
- En el directorio
node_modules
más cercano - Si no lo encuentra, sube un nivel y busca en el siguiente
node_modules
- 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
yclassic
, 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';
- TypeScript compila a JavaScript:
// archivo.js (compilado)
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const otro_archivo_1 = require("./otro-archivo");
- 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
entsconfig.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:
- El directorio actual y sus padres
- En
node_modules/@types/[nombre-paquete]
- 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á:
./button.ios.ts
./button.native.ts
./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:
- Archivos de declaración adyacentes (
utils.d.ts
) - JSDoc en el archivo JavaScript para inferir tipos
- 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
ymoduleResolution
- 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:
- Crear un archivo
.d.ts
en nuestro proyecto - Definir los tipos necesarios usando
declare
- 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
detsconfig.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.
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
Reto composición de funciones
Reto tipos especiales
Reto tipos genéricos
Módulos
Polimorfismo
Funciones TypeScript
Interfaces
Funciones puras
Reto namespaces
Funciones flecha
Polimorfismo
Operadores
Conversor de unidades
Funciones flecha
Control de flujo
Herencia
Clases
Proyecto validación de tipado
Clases y objetos
Encapsulación
Herencia
Proyecto sistema de votación
Reto genéricos con clases
Inmutabilidad
Interfaces
Funciones de alto orden
Reto map y filter
Control de flujo
Interfaces
Reto funciones orden superior
Herencia y clases abstractas
Reto tipos mapped
Herencia de clases
Reto funciones puras
Variables y constantes
Introducción a TypeScript
Reto testing unitario
Funciones de primera clase
Clases
OOP y CRUD en TypeScript
Interfaces y su implementación
Tipos genéricos
Namespaces
Operadores y expresiones
Proyecto generador de contraseñas
Reto unión e intersección
Encapsulación
Tipos de unión e intersección
Tipos de unión e intersección
Reto hola mundo en TS
Variables y constantes
Funciones puras
Control de flujo
Introducción a TypeScript
Resolución de módulos
Control de flujo
Reto tipos de utilidad
Reto tipos literales y condicionales
Reto exportar e importar
Propiedades y métodos
Tipos de utilidad
Clases y objetos
Tipos de datos, variables y constantes
Proyecto Minigestor de tareas
Operadores
Funciones flecha y contexto
Proyecto Inventario de productos
Funciones
Reto type aliases
Funciones de alto orden
Funciones y parámetros tipados
Tipos literales
Reto enums
Tipos de utilidad
Modificadores de acceso y encapsulación
Polimorfismo
Tipos genéricos
Reto módulos
Tipos literales
Inmutabilidad
Proyecto Generator de datos
Variables y constantes
Funciones de primera clase
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
Introducción Y Entorno
Instalación Y Configuración De Typescript
Introducción Y Entorno
Tipos De Datos, Variables Y Constantes
Sintaxis
Operadores Y Expresiones
Sintaxis
Control De Flujo
Sintaxis
Funciones Y Parámetros Tipados
Sintaxis
Funciones Flecha Y Contexto
Sintaxis
Enums
Sintaxis
Type Aliases Y Aserciones De Tipo
Sintaxis
Clases Y Objetos
Programación Orientada A Objetos
Interfaces Y Su Implementación
Programación Orientada A Objetos
Modificadores De Acceso Y Encapsulación
Programación Orientada A Objetos
Herencia Y Clases Abstractas
Programación Orientada A Objetos
Polimorfismo
Programación Orientada A Objetos
Decoradores Básicos
Programación Orientada A Objetos
Propiedades Y Métodos
Programación Orientada A Objetos
Inmutabilidad
Programación Funcional
Funciones Puras Y Efectos Secundarios
Programación Funcional
Funciones De Primera Clase
Programación Funcional
Funciones De Alto Orden
Programación Funcional
Conceptos Básicos E Inmutabilidad
Programación Funcional
Funciones De Primera Clase Y Orden Superior
Programación Funcional
Composición De Funciones
Programación Funcional
Métodos Funcionales De Arrays (Map, Filter, Reduce)
Programación Funcional
Tipos Literales Y Tipos Condicionales
Tipos Intermedios Y Avanzados
Tipos Genéricos Básicos
Tipos Intermedios Y Avanzados
Tipos De Unión E Intersección
Tipos Intermedios Y Avanzados
Tipos De Utilidad (Partial, Required, Pick, Etc)
Tipos Intermedios Y Avanzados
Unknown, Never Y Tipos Especiales
Tipos Intermedios Y Avanzados
Tipos Mapped
Tipos Intermedios Y Avanzados
Genéricos Con Clases E Interfaces
Tipos Intermedios Y Avanzados
Módulos
Namespaces Y Módulos
Namespaces
Namespaces Y Módulos
Resolución De Módulos
Namespaces Y Módulos
Exportación E Importación De Módulos
Namespaces Y Módulos
Introducción A Módulos
Namespaces Y Módulos
Testing Unitario En Typescript
Testing
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.