🇺🇸 EN
TypeScript
·7 min read·↗·🇺🇸 EN

Cómo crear definiciones de tipos en TypeScript (.d.ts)

Portada principal del artículo: Cómo crear definiciones de tipos en TypeScript (.d.ts)

Todos hemos vivido este momento en TypeScript. Instalas un paquete de npm muy útil, lo importas en tu código y el compilador de TypeScript te detiene de inmediato con un error:

Could not find a declaration file for module 'old-library'.
'node_modules/old-library/index.js' implicitly has an 'any' type.
Try `npm i --save-dev @types/old-library` if it exists.

Intentas ejecutar npm i -D @types/old-library, pero npm te responde que el paquete no existe en DefinitelyTyped.

Muchos desarrolladores responden agregando // @ts-ignore o forzando la importación como any. Eso elimina la advertencia del compilador, pero destruye la seguridad de tipos y el autocompletado para todo tu equipo.

La solución limpia es escribir tu propio archivo de definición de tipos (.d.ts). En este artículo, te mostraré cómo declarar tipos para paquetes de npm sin tipado, cómo configurar tsconfig.json y cómo extender objetos globales como window y process.env.


1. ¿Qué son los archivos de declaración (.d.ts)?

TypeScript separa la lógica ejecutable de las descripciones de tipos. Los archivos .ts habituales compilan a archivos .js. En cambio, los archivos .d.ts contienen únicamente metadatos de tipos:

  • Nunca generan código JavaScript de salida.
  • Describen firmas de funciones, formas de clases y propiedades de objetos.
  • Usan la palabra clave declare para informarle a TypeScript que la implementación real existe en otro lugar en tiempo de ejecución.

Antes de escribir firmas completas, veamos la forma más rápida de solucionar el error y permitir que tu proyecto compile.


2. Solución rápida: declaraciones ambientales abreviadas

Si necesitas que tu proyecto compile de inmediato, puedes crear una declaración de módulo ambiental abreviada.

Crea una carpeta en tu proyecto llamada src/types/ y agrega un archivo llamado untyped-modules.d.ts:

// src/types/untyped-modules.d.ts
declare module 'old-library';

Esta sola línea le dice a TypeScript: "Confía en mí, 'old-library' existe en tiempo de ejecución. Trata cualquier elemento importado desde allí como any."

import { calculateTotal } from 'old-library';
 
// calculateTotal está tipado como any, pero la compilación tiene éxito
const result = calculateTotal(10, 20);

Este atajo soluciona el error del compilador al instante. Sin embargo, no obtienes autocompletado ni validación de parámetros. Para conseguir una seguridad de tipos completa, necesitamos escribir definiciones de tipos específicas.


3. Escribir declaraciones de módulo completas

Imaginemos que tenemos un paquete de utilidades HTTP sin tipos llamado simple-fetcher.

La librería expone un objeto cliente con get, post y opciones de solicitud personalizadas. Queremos autocompletado total en nuestro editor de código.

Crea un archivo llamado src/types/simple-fetcher.d.ts:

// src/types/simple-fetcher.d.ts
 
declare module 'simple-fetcher' {
  export interface RequestOptions {
    timeout?: number;
    headers?: Record<string, string>;
  }
 
  export interface ApiResponse<T> {
    data: T;
    status: number;
    ok: boolean;
  }
 
  export function get<T = unknown>(
    url: string,
    options?: RequestOptions
  ): Promise<ApiResponse<T>>;
 
  export function post<T = unknown>(
    url: string,
    body: unknown,
    options?: RequestOptions
  ): Promise<ApiResponse<T>>;
}

Cada elemento exportado coincide con el comportamiento en JavaScript en tiempo de ejecución del paquete. Ahora, cuando usamos la librería, nuestro editor nos brinda autocompletado y validación estricta de parámetros:

import { get, RequestOptions } from 'simple-fetcher';
 
interface User {
  id: string;
  name: string;
}
 
const config: RequestOptions = { timeout: 5000 };
 
// ¡TypeScript sabe que response.data es User!
const response = await get<User>('https://api.example.com/user/1', config);
console.log(response.data.name);

Si pasas una opción inválida (como timeout: "fast"), el compilador detecta el error de inmediato antes de que tu código llegue a producción.

Ahora que nuestro archivo de tipos está listo, debemos asegurarnos de que el compilador de TypeScript realmente lo cargue.


4. Configurar tsconfig.json para incluir tipos locales

Por defecto, TypeScript busca definiciones de tipos dentro de node_modules/@types.

Para que TypeScript reconozca tus archivos .d.ts personalizados, actualiza tu tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2022",
    "moduleResolution": "bundler",
    "strict": true,
    "typeRoots": [
      "./node_modules/@types",
      "./src/types"
    ]
  },
  "include": [
    "src/**/*",
    "src/types/**/*.d.ts"
  ]
}

Hay dos opciones de configuración importantes aquí:

  • typeRoots: Le indica a TypeScript qué directorios contienen definiciones de tipos ambientales. Mantén siempre ./node_modules/@types para que los tipos de npm instalados sigan funcionando.
  • include: Asegura que el compilador incluya tus archivos .d.ts durante la compilación.

Con tsconfig.json configurado, tus definiciones personalizadas se aplican en todo el proyecto.

Ahora bien, ¿qué pasa cuando un paquete ya tiene tipos, pero necesitas agregar métodos personalizados? Para eso usamos module augmentation.


5. Module Augmentation: extender librerías existentes

A veces una librería ya incluye tipos, pero tu aplicación agrega un plugin o middleware que añade nuevas propiedades a sus objetos.

Por ejemplo, imagina que usas Express o Fastify, y tu middleware de autenticación adjunta un currentUser al objeto de la solicitud.

Puedes extender los tipos de la librería dentro de un archivo .d.ts:

// src/types/express-augmented.d.ts
import 'express';
 
declare module 'express-serve-static-core' {
  interface Request {
    currentUser?: {
      id: string;
      email: string;
      role: 'admin' | 'member';
    };
  }
}

Nota la sentencia import 'express' en la parte superior. Esta importación convierte el archivo en un módulo externo. Esto le indica a TypeScript que combine tu nueva propiedad currentUser con la interfaz Request existente en lugar de reemplazarla.

En tus controladores de rutas, la nueva propiedad ahora es reconocida:

import { Request, Response } from 'express';
 
export function profileHandler(req: Request, res: Response) {
  if (!req.currentUser) {
    return res.status(401).send('Unauthorized');
  }
 
  // ¡TypeScript sabe que currentUser tiene id, email y role!
  res.json({ userId: req.currentUser.id, role: req.currentUser.role });
}

Este mismo patrón funciona para extender objetos globales de JavaScript como window o process.env.


6. Extender objetos globales (window y process.env)

En entornos de navegador y de Node.js, con frecuencia necesitamos acceder a variables de entorno o scripts de terceros adjuntos al ámbito global.

Agregar propiedades personalizadas al objeto window del navegador

Si integras un script de analítica o un SDK de pagos (como Google Tag Manager o Stripe), este se adjunta al objeto window:

// src/types/global.d.ts
export {};
 
declare global {
  interface Window {
    dataLayer: Array<Record<string, unknown>>;
    analyticsScriptLoaded?: boolean;
  }
}

El export {}; en la parte superior asegura que TypeScript trate el archivo como un módulo. Dentro de declare global, cualquier interfaz que declares se combina con los tipos estándar del DOM del navegador.

Ahora puedes escribir window.dataLayer.push(...) sin ninguna queja de TypeScript.

Tipar process.env en Node.js

En lugar de ver string | undefined cada vez que accedes a las variables de entorno, puedes tipar process.env:

// src/types/environment.d.ts
export {};
 
declare global {
  namespace NodeJS {
    interface ProcessEnv {
      NODE_ENV: 'development' | 'production' | 'test';
      DATABASE_URL: string;
      PORT?: string;
      API_SECRET_KEY: string;
    }
  }
}

Ahora, cada vez que leas process.env.DATABASE_URL, TypeScript sabrá que existe y autocompletará las claves válidas.


7. Comparación: cuándo usar cada técnica de declaración de tipos

Aquí tienes una tabla de referencia rápida para ayudarte a elegir la técnica adecuada:

EnfoqueUbicación del archivoCuándo usarloSeguridad de tipos resultante
Ambient abreviadosrc/types/*.d.tsSolución rápida para destrabar compilaciones de CI/CDBaja (any)
Módulo completosrc/types/*.d.tsPaquetes de npm sin tipos y sin DefinitelyTypedAlta (tipos estrictos)
Module Augmentationsrc/types/*.d.tsAgregar plugins o middleware a librerías con tiposAlta (fusión de interfaces)
Global Augmentationsrc/types/*.d.tsTipar window, process.env o scripts globalesAlta (ámbito global)

Conclusión y siguientes pasos

Escribir archivos de declaración .d.ts personalizados es una de las habilidades más prácticas que puedes tener en TypeScript:

  1. Evita any y @ts-ignore: Un archivo de declaración breve toma cinco minutos de escribir y protege toda tu base de código.
  2. Comienza simple: Usa declaraciones ambientales abreviadas para solucionar la compilación y luego agrega funciones e interfaces explícitas según sea necesario.
  3. Usa augmentation: Extiende interfaces de terceros existentes y objetos globales usando declare module y declare global.

Si quieres continuar fortaleciendo tu arquitectura en TypeScript, echa un vistazo a estas guías relacionadas:

Parte de la serie TypeScript

Mastering the language of the web. From basic types to advanced generics and patterns.

Ver serie completa

Preguntas Frecuentes

¿Qué es un archivo .d.ts en TypeScript?

Un archivo .d.ts es un archivo de declaración que contiene únicamente información de tipos sin implementación en tiempo de ejecución. Le indica al compilador de TypeScript la estructura, funciones y clases del código JavaScript.

¿Cómo soluciono el error 'Could not find a declaration file for module'?

Primero verifica si existe un paquete de la comunidad con npm install -D @types/package-name. Si no existe, crea un archivo de declaración local con declare module 'package-name' y agrega su ruta en tu tsconfig.json.

¿Qué es module augmentation en TypeScript?

Module augmentation te permite extender definiciones de tipos existentes de librerías de terceros o módulos internos sin modificar sus archivos fuente originales.

¿Cómo agrego propiedades personalizadas a window o process.env?

Puedes usar declare global en un archivo .d.ts para extender la interfaz Window o la interfaz NodeJS.ProcessEnv con tus variables personalizadas.

Artículos Relacionados

Compartir este artículo

Si esta guía te resultó útil, compártela con tu equipo o con otros desarrolladores.


Software real. Lecciones reales.

Comparto las lecciones que aprendí por las malas, para que puedas evitarlas o estar preparado cuando ocurran.

User avatar
User avatar
User avatar
User avatar
+13K

Únete a 13,800+ desarrolladores y lectores.

Cero spam. Cancela tu suscripción en cualquier momento.