tutoriales.com

Desentrañando los Módulos de Declaración en TypeScript: Globales vs. de Módulo

Este tutorial explora a fondo los módulos de declaración en TypeScript, una herramienta esencial para trabajar con librerías externas o código JavaScript existente. Cubriremos la diferencia entre declaraciones globales y de módulo, y te guiaremos sobre cómo usarlas correctamente para mejorar el tipado de tus proyectos.

Intermedio18 min de lectura7 views
Reportar error

🚀 Introducción a los Módulos de Declaración en TypeScript

TypeScript es una maravilla para el desarrollo, pero a menudo nos encontramos con la necesidad de integrar código JavaScript plain o librerías externas que no fueron escritas con TypeScript en mente. Aquí es donde entran en juego los módulos de declaración (Declaration Modules), también conocidos como archivos d.ts.

Estos archivos son la clave para "enseñar" a TypeScript la forma de un código existente, permitiéndonos disfrutar de autocompletado, verificación de tipos y refactorización segura, incluso para código no TypeScript. En este tutorial, desglosaremos las dos categorías principales: las declaraciones globales y las declaraciones de módulo, y te mostraremos cuándo y cómo usar cada una.

💡 Sabías que: La extensión .d.ts significa "declaration TypeScript" y es exclusiva para archivos que contienen solo declaraciones de tipo, sin implementación de código real.


📌 ¿Qué son los Archivos .d.ts y por qué son cruciales?

Los archivos .d.ts son esencialmente "planos" o "contratos" que describen la forma de un código JavaScript existente. No contienen ninguna lógica de ejecución, solo definiciones de tipos. Su propósito principal es permitir que el compilador de TypeScript entienda la estructura de las variables, funciones, clases y objetos definidos en archivos .js.

¿Por qué son cruciales?

  • Interoperabilidad: Nos permiten usar librerías JS populares (como jQuery, Lodash, React sin tipado explícito, etc.) con los beneficios de TypeScript.
  • Refactorización segura: El compilador puede advertirnos si cambiamos el código tipado de una manera que rompe la compatibilidad con el código JS subyacente.
  • Autocompletado y IntelliSense: Mejoran drásticamente la experiencia del desarrollador en IDEs como VS Code, ofreciendo sugerencias de código precisas.
  • Detección de errores en tiempo de compilación: Ayudan a encontrar errores relacionados con tipos antes de que el código se ejecute.
🔥 **Importante:** Un archivo `.d.ts` nunca genera código JavaScript. Solo existe para el compilador de TypeScript.

🗺️ Módulos de Declaración Globales: Ampliando el Universo Global

Las declaraciones globales se usan para tipar variables, funciones o clases que están disponibles globalmente en el entorno de ejecución, es decir, que no están encapsuladas en un módulo. Piensa en variables como window, document o librerías antiguas que inyectan sus objetos directamente en el ámbito global (por ejemplo, jQuery usando $ o jQuery).

Cuándo usar declaraciones globales:

  • Cuando la librería no usa un sistema de módulos (CommonJS, ES Modules).
  • Cuando necesitas extender tipos globales existentes (como window o Math).
  • Para scripts que simplemente se cargan en la página y exponen sus funcionalidades globalmente.

Sintaxis básica de declaración global

Un archivo .d.ts que no contiene import ni export de nivel superior se considera una declaración global. Puedes declarar variables, funciones, clases, interfaces y tipos directamente.

// globals.d.ts

declare var MY_GLOBAL_VAR: string;
declare function globalFunction(name: string): void;

interface GlobalConfig {
  appName: string;
  version: number;
}
declare var CONFIG: GlobalConfig;

// Extender un tipo global existente, por ejemplo, window
interface Window {
  myCustomProperty: number;
  analytics: {
    trackEvent(eventName: string, data?: object): void;
  };
}

Después de definir globals.d.ts, TypeScript conocerá estos tipos globalmente. Puedes acceder a MY_GLOBAL_VAR o window.myCustomProperty en cualquier archivo .ts de tu proyecto sin necesidad de importar nada.

Ejemplo práctico: Tipando una librería global antigua

Imaginemos que tenemos una librería JavaScript muy antigua, legacy-library.js, que expone una función calculateSum y una variable APP_VERSION globalmente.

// legacy-library.js

var APP_VERSION = '1.0.0';

function calculateSum(a, b) {
  console.log('Calculating sum...');
  return a + b;
}

// Simular que se añade algo al window
window.myLegacyUtility = {
  showMessage: function(msg) { console.log('Legacy Msg: ' + msg); }
};

Para tipar esto en TypeScript, crearíamos un archivo legacy-library.d.ts (o lo incluiríamos en un archivo globals.d.ts).

// legacy-library.d.ts

declare var APP_VERSION: string;
declare function calculateSum(a: number, b: number): number;

interface Window {
  myLegacyUtility: {
    showMessage(msg: string): void;
  };
}

Ahora, en cualquier archivo .ts:

// app.ts

console.log(APP_VERSION); // TypeScript sabe que es un string
const result = calculateSum(5, 10); // TypeScript sabe los tipos de los argumentos y el retorno
console.log(`The sum is: ${result}`);

window.myLegacyUtility.showMessage('Hello from TS!');
// window.myLegacyUtility.showMessage(123); // Error de tipo: Argument of type '123' is not assignable to parameter of type 'string'.
⚠️ Advertencia: El abuso de declaraciones globales puede llevar a colisiones de nombres y a un código difícil de mantener. Úsalas con moderación y solo cuando sea estrictamente necesario.

📦 Módulos de Declaración de Módulo: Tipando Librerías con import/export

Las declaraciones de módulo son mucho más comunes en el desarrollo moderno, ya que la mayoría de las librerías JavaScript actuales utilizan sistemas de módulos (CommonJS para Node.js o ES Modules para el navegador). Estas declaraciones se usan para tipar módulos específicos que son importados o exportados.

Cuándo usar declaraciones de módulo:

  • Cuando la librería exporta sus funcionalidades usando export (ES Modules) o module.exports (CommonJS).
  • Para librerías instaladas vía npm que no incluyen sus propios tipos (@types/*).
  • Cuando necesitas augmentar (extender) un módulo existente o un tipo de módulo.

Sintaxis básica de declaración de módulo (declare module)

Las declaraciones de módulo se crean con la sintaxis declare module 'module-name' { ... }. El module-name debe coincidir con el nombre que usarías en una sentencia import.

// my-untyped-library.d.ts

declare module 'my-untyped-library' {
  export function doSomething(param: string): boolean;
  export const VERSION: string;

  export interface LibraryOptions {
    timeout: number;
    retries: number;
  }

  export class LibraryClient {
    constructor(options: LibraryOptions);
    fetchData(): Promise<any>;
  }

  // Declarar un export default si la librería lo tiene
  export default function setupLibrary(): void;
}

Ahora, en tu código TypeScript:

// app.ts

import { doSomething, VERSION, LibraryClient } from 'my-untyped-library';
import setup from 'my-untyped-library'; // Para el export default

console.log(VERSION);
doSomething('hello');
// doSomething(123); // Error de tipo

const client = new LibraryClient({ timeout: 5000, retries: 3 });
client.fetchData().then(data => console.log(data));

setup();

📦 Declaraciones module para archivos específicos (*.svg, *.json)

Una variante útil de las declaraciones de módulo es para importar tipos de archivos no-JavaScript, como .svg, .png, .json, etc. Esto es común en frameworks como React con Webpack/Vite, donde puedes importar recursos directamente.

// image.d.ts (o dentro de un global.d.ts)

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

declare module '*.svg' {
  import * as React from 'react';
  export const ReactComponent: React.FunctionComponent<React.SVGProps
& { title?: string }>; const src: string; export default src; } declare module '*.json' { const value: any; export default value; } ``` Esto permite importaciones como: ```typescript // app.ts import logo from './logo.svg'; import { ReactComponent as LogoIcon } from './logo.svg'; import config from './config.json'; function MyComponent() { return (
Logo

App name: {config.appName}

); } ``` ### Extensión de Módulos (Module Augmentation) A veces, una librería ya tiene sus propios tipos (por ejemplo, con `@types/node` o `@types/express`), pero necesitas añadirle propiedades o métodos que no están incluidos en sus definiciones originales. Aquí es donde entra la extensión de módulos. ```typescript // custom-express.d.ts // Si el módulo usa export default, usa 'export default interface ...' // Si el módulo exporta una interfaz o clase con nombre, usa 'declare module ... { interface ... }' // Ejemplo para Express: añadir una propiedad 'user' al objeto Request declare namespace Express { interface Request { user?: { id: string; name: string }; sessionId: string; } } // Ejemplo para un módulo con nombre declare module 'my-library' { // Aquí puedes añadir o modificar exports existentes interface ExistingType { newProperty: boolean; } export function newUtilityFunction(): void; } ``` Con esto, en tu código Express: ```typescript // server.ts import express from 'express'; const app = express(); app.use((req, res, next) => { // Ahora req.user y req.sessionId existen y están tipados req.user = { id: '123', name: 'John Doe' }; req.sessionId = 'abc-123'; next(); }); app.get('/profile', (req, res) => { if (req.user) { res.send(`Welcome, ${req.user.name}! Session: ${req.sessionId}`); } else { res.status(401).send('Unauthorized'); } }); app.listen(3000, () => console.log('Server running on port 3000')); ``` --- ## 🆚 Global vs. Módulo: ¿Cuándo usar cuál? 🎯 La elección entre declaraciones globales y de módulo es fundamental para mantener tu base de código organizada y evitar problemas. Aquí una tabla comparativa y un diagrama para clarificar. | Característica | Declaración Global | Declaración de Módulo | | :-------------------- | :---------------------------------------- | :------------------------------------------------ | | **Sintaxis** | `declare var/function/interface/class` | `declare module 'module-name' { ... }` | | **Uso principal** | Librerías antiguas, scripts globales, extender tipos intrínsecos (`window`, `Math`) | Librerías modernas con `import`/`export`, archivos no-JS, extensión de módulos npm | | **Ámbito** | Global, disponible en todo el proyecto sin importación | Local al módulo declarado, requiere `import` | | **Colisiones** | Mayor riesgo de colisiones de nombres | Menor riesgo, nombres encapsulados por el módulo | | **Archivos .d.ts** | No contienen `import`/`export` de nivel superior | Contienen `declare module '...'` o pueden exportar directamente sus tipos si son el propio módulo | | **Ejemplo** | `declare var jQuery: JQueryStatic;` | `declare module 'lodash' { export function ... }` | Necesito tipar código JS ¿El código JS expone variables o funciones globalmente? Usar declaración Global (.d.ts sin imports/exports de nivel superior) No ¿Usa import/export o es una librería instalada con npm? Usar declaración de Módulo declare module 'nombre' { ... } No ¿Es un archivo no-JS como .svg o .json? Usar declaración de Módulo declare module '*.ext' { ... } No Evaluar caso específico o reestructurar JS
💡 Consejo: Siempre que sea posible, prefiere las **declaraciones de módulo**. Son más seguras, evitan el riesgo de polución del ámbito global y son la forma estándar de trabajar con librerías modernas en TypeScript.

🛠️ Organización de los Archivos .d.ts

La forma en que organizas tus archivos de declaración puede impactar la mantenibilidad de tu proyecto.

  1. Para librerías instaladas (npm): Idealmente, la librería ya viene con sus tipos (@types/* o incluidos). Si no, crea un archivo .d.ts en tu proyecto, por ejemplo, src/types/my-untyped-lib.d.ts.
  2. Para scripts globales: Puedes tener un único archivo global.d.ts o varios, por ejemplo, src/types/legacy-globals.d.ts, src/types/window-augmentations.d.ts.
  3. Para archivos no-JS: Un archivo src/types/file-modules.d.ts o global.d.ts es un buen lugar.

Configuración de tsconfig.json

TypeScript necesita saber dónde buscar estos archivos de declaración. Generalmente, el compilador los detecta automáticamente si están en tu rootDir o en directorios como node_modules/@types. Sin embargo, puedes especificar rutas explícitamente.

// tsconfig.json
{
  "compilerOptions": {
    // ... otras opciones
    "typeRoots": ["./node_modules/@types", "./src/types"], // Donde buscar archivos d.ts
    "types": [] // Si lo dejas vacío, TypeScript incluirá todos los de typeRoots
  },
  "include": ["src/**/*.ts", "src/**/*.d.ts"]
}
  • typeRoots: Especifica un array de directorios desde los cuales se buscarán los paquetes @types/*. También puedes incluir tus propios directorios de declaraciones aquí.
  • types: Si se especifica, solo los paquetes listados en este array (y sus dependencias) serán incluidos. Si se deja vacío (o no se especifica), TypeScript buscará todos los paquetes en typeRoots.
  • include: Asegúrate de que tus archivos .d.ts estén incluidos en la compilación. Si están en el rootDir, suelen ser detectados automáticamente, pero incluir explícitamente src/**/*.d.ts es una buena práctica.

✨ Casos Avanzados y Buenas Prácticas

Declaraciones de módulos con subrutas

Algunas librerías permiten importar submódulos (ej. lodash/fp). Puedes tipar esto también:

// lodash-fp-custom.d.ts

declare module 'lodash/fp' {
  export function map<T, U>(iteratee: (item: T) => U): (array: T[]) => U[];
  export function filter<T>(predicate: (item: T) => boolean): (array: T[]) => T[];
  // ... otras funciones específicas de lodash/fp
}

Declarar módulos sin nombre (wildcard module declarations)

Útil para módulos que no tienen un nombre de archivo fijo o para recursos genéricos, como en el ejemplo de .png y .svg.

// custom-types.d.ts

declare module '*.scss' {
  const content: Record<string, string>;
  export default content;
}

declare module '*.less' {
  const content: Record<string, string>;
  export default content;
}

Esto te permitirá importar estilos CSS Modules en React, por ejemplo:

import styles from './MyComponent.module.scss';

console.log(styles.container);

Evitando la polución global con export {}

Si creas un archivo .d.ts que solo contiene declaraciones globales pero quieres asegurarte de que TypeScript lo trate como un módulo (y evitar que sus declaraciones afecten el ámbito global si se interpreta incorrectamente), puedes añadir un export {} vacío al final.

// some-global-declarations.d.ts

declare const SOME_GLOBAL_SETTING: boolean;
declare interface CustomEventMap {
  'my-custom-event': { detail: string };
}

// Esto asegura que el archivo sea tratado como un módulo, 
// pero las declaraciones anteriores aún son globales.
export {};

Esto es un truco para garantizar que TypeScript no malinterprete un archivo que parece global como un script si, por ejemplo, tienes una configuración de moduleResolution que podría afectar esto. Sin embargo, para declaraciones puramente globales que quieres que estén en el ámbito global, no es necesario.

⚠️ Peligros de las Declaraciones any o unknown

Aunque es tentador usar any o unknown para tipar rápidamente un módulo complejo, esto anula muchos de los beneficios de TypeScript. Intenta ser lo más específico posible con tus tipos. Si tienes que usar any o unknown, aísla su uso y documéntalo claramente.

90% de Especificidad Buscada

🔚 Conclusión

Los módulos de declaración son una característica poderosa y esencial de TypeScript que te permite integrar sin problemas código JavaScript existente, librerías sin tipos y recursos no-JS en tus proyectos tipados. Entender la diferencia entre las declaraciones globales y las de módulo, y saber cuándo usar cada una, es clave para mantener un código robusto, legible y fácil de mantener.

Al aplicar los conocimientos de este tutorial, estarás mejor equipado para aprovechar al máximo TypeScript en cualquier entorno, garantizando la seguridad de tipos y una experiencia de desarrollo superior. ¡Feliz tipado!

Tutoriales relacionados

Comentarios (0)

Aún no hay comentarios. ¡Sé el primero!