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.
🚀 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.tssignifica "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.
🗺️ 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
windowoMath). - 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'.
📦 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) omodule.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
App name: {config.appName}
🛠️ Organización de los Archivos .d.ts
La forma en que organizas tus archivos de declaración puede impactar la mantenibilidad de tu proyecto.
- Para librerías instaladas (npm): Idealmente, la librería ya viene con sus tipos (
@types/*o incluidos). Si no, crea un archivo.d.tsen tu proyecto, por ejemplo,src/types/my-untyped-lib.d.ts. - Para scripts globales: Puedes tener un único archivo
global.d.tso varios, por ejemplo,src/types/legacy-globals.d.ts,src/types/window-augmentations.d.ts. - Para archivos no-JS: Un archivo
src/types/file-modules.d.tsoglobal.d.tses 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 entypeRoots.include: Asegúrate de que tus archivos.d.tsestén incluidos en la compilación. Si están en elrootDir, suelen ser detectados automáticamente, pero incluir explícitamentesrc/**/*.d.tses 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
any o unknownAunque 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.
🔚 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
- Tipado Avanzado de Redux y Redux Toolkit con TypeScript: Una Guía Completaintermediate15 min
- Tipos Utilitarios en TypeScript: Potenciando Tu Código con Mapped Types y Condicionalesadvanced18 min
- Dominando los Decoradores en TypeScript: Una Guía Práctica con Ejemplos Realesintermediate15 min
- Tipado de Eventos en el DOM con TypeScript: Guía Completa para Interfaces y Manejadoresintermediate10 min
Comentarios (0)
Aún no hay comentarios. ¡Sé el primero!