Un archivo de declaraciones (.d.ts) contiene tipos y nada más: firmas, interfaces y formas de clases para JavaScript que vive en otro sitio. La palabra clave declare hace el mismo trabajo dentro de un archivo normal. Le dice al compilador «esto existe en tiempo de ejecución, confía en mí», y no genera código.
El compilador aceptó __APP_VERSION__.length porque la declaración dice que es un string. En tiempo de ejecución esa variable no existe, así que leer .length lanza ReferenceError: __APP_VERSION__ is not defined, una excepción en tiempo de ejecución que el compilador nunca vio venir. Ese es el contrato de toda declaración: los tipos solo son tan ciertos como el JavaScript que hay detrás.
Qué va en un archivo .d.ts
Compilar con declaration: true es la mejor manera de verlo. A partir de este código:
// price.ts
export interface LineItem {
name: string;
price: number;
qty: number;
}
const TAX = 0.2;
export function total(items: LineItem[]) {
const sum = items.reduce((acc, item) => acc + item.price * item.qty, 0);
return Math.round(sum * (1 + TAX) * 100) / 100;
}
export class Cart {
private items: LineItem[] = [];
add(item: LineItem) {
this.items.push(item);
return this;
}
}
tsc escribe price.js y este price.d.ts:
export interface LineItem {
name: string;
price: number;
qty: number;
}
export declare function total(items: LineItem[]): number;
export declare class Cart {
private items;
add(item: LineItem): this;
}
Los cuerpos de las funciones desaparecen, los tipos de retorno inferidos se escriben (number, this), el campo privado conserva su nombre pero pierde su tipo, y el TAX no exportado no aparece. Una librería publica el .js para Node y el .d.ts para tu editor y tu compilador. emitDeclarationOnly: true produce solo los archivos .d.ts, para proyectos en los que un bundler genera el JavaScript.
Los objetos integrados de ES2022 que usas a diario también vienen de archivos de declaraciones: lib.es2022.d.ts y compañía vienen con TypeScript y se seleccionan con target y lib.
Las formas de declare
Cada sentencia declare describe algo que ya existe. En un archivo .d.ts sin import ni export (un archivo de declaraciones global), cada una de estas se vuelve visible para todo el proyecto:
// globals.d.ts
declare const API_URL: string; // a global constant
declare let debugMode: boolean; // a global variable
declare function track(event: string, props?: Record<string, string>): void;
declare class Widget { // a class from a script tag
constructor(el: string);
render(): void;
}
declare namespace Analytics { // a global object
function page(name: string): void;
}
declare module "legacy-charts" { // a module you import
export function draw(data: number[]): void;
}
Dentro de un archivo .d.ts, interface y type no necesitan declare; cualquier otra declaración de nivel superior sin declare ni export es el error TS1046. En cuanto un archivo tiene un import o export, sus declaraciones son locales a él, y lo que se añade al ámbito global va en un bloque declare global (se muestra más abajo). La firma declarada es contra la que el compilador comprueba las llamadas:
El compilador informa index.ts(5,21): error TS2322: Type 'number' is not assignable to type 'string'. Pasa { items: "3" }, o cambia la declaración si la función real acepta números.
Paquetes @types
Muchos paquetes de npm incluyen sus propios archivos .d.ts, referenciados desde el campo types (o una condición types en exports) de su package.json. Para los paquetes que solo tienen JavaScript, el proyecto comunitario DefinitelyTyped publica tipos bajo el scope @types:
npm i lodash
npm i -D @types/lodash
Cuando haces import de un paquete, TypeScript busca node_modules/@types/{name} automáticamente. Los tipos que describen globales en lugar de un import, como @types/node (process, Buffer) o el describe y el it de un test runner, deben aparecer en tsconfig.json:
{
"compilerOptions": {
"types": ["node"]
}
}
Sin esa entrada, TypeScript 7 no los carga, aunque el paquete esté instalado:
error TS2591: Cannot find name 'process'. Do you need to install type definitions for node? Try `npm i --save-dev @types/node` and then add 'node' to the types field in your tsconfig.
tsc --init escribe "types": [] en la configuración nueva, con un comentario que sugiere ["node"] para proyectos de Node.
Tipar un módulo sin tipos
Importar un paquete de JavaScript que no tiene tipos, ni incluidos ni en @types, es el error TS7016:
error TS7016: Could not find a declaration file for module 'fakelib'. '/project/node_modules/fakelib/index.js' implicitly has an 'any' type.
Arréglalo con un archivo .d.ts en cualquier lugar del proyecto (es habitual una carpeta types/; solo tiene que quedar cubierta por include) sin import ni export de nivel superior. Describe las partes que usas:
// types/fakelib.d.ts
declare module "fakelib" {
export function hi(name: string): string;
export const version: string;
}
// Non-code files a bundler lets you import
declare module "*.svg" {
const url: string;
export default url;
}
La versión más corta, declare module "fakelib"; en una sola línea, hace que todo import del paquete sea any. Elimina el error y con él todas las comprobaciones, así que tómalo como un paso temporal.
declare global
El código que añade cosas al ámbito global, como un polyfill, un método nuevo en un objeto integrado o una global definida por una etiqueta script, necesita los tipos correspondientes. declare global los añade. Tiene que estar en un módulo (un archivo con un import o export), por eso hay un export {} al principio:
interface Array<T> se fusiona con la interfaz integrada Array en lugar de sustituirla, y var (no let ni const) es lo que añade una propiedad a globalThis. Ampliar prototipos integrados es arriesgado en código compartido; la misma técnica de declare global es la que usan los proyectos para añadir sus propias variables a process.env en la interfaz NodeJS.ProcessEnv de @types/node.
Ampliación de módulos
Para añadir algo a los tipos de un paquete que importas, vuelve a declarar el nombre de su módulo y reabre la interfaz. El archivo tiene que ser un módulo (el import se encarga de eso; export {} también sirve). Sin un import ni un export, el mismo bloque declara un módulo config-lib totalmente nuevo que oculta los tipos reales del paquete:
// types/config-lib.d.ts
import "config-lib";
declare module "config-lib" {
interface Settings {
beta: boolean; // merged into the package's own Settings interface
}
}
Después de esto, load().beta de config-lib tiene tipo boolean en todas partes. Las interfaces se fusionan y los alias de tipo no, así que la librería tiene que exportar una interface para que esto funcione. Así es como los plugins añaden campos a los objetos de petición o de configuración de un framework.
skipLibCheck
skipLibCheck: true hace que el compilador no compruebe los tipos de los archivos .d.ts, incluidos los de node_modules. Tu propio código se sigue comprobando contra ellos. Ahorra tiempo y evita errores entre dos paquetes cuyas declaraciones no concuerdan, y por eso tsc --init lo activa. El precio es que tampoco se informa de un error dentro de tus propios archivos .d.ts.
Preguntas frecuentes
¿Qué es un archivo .d.ts en TypeScript?
Un archivo de declaraciones: contiene solo tipos (firmas de funciones, interfaces, formas de clases) y ninguna implementación. Describe JavaScript que existe en otro sitio, como una librería compilada o las APIs integradas del navegador, para que TypeScript pueda comprobar el código que lo usa. tsc nunca genera JavaScript para él.
¿Qué hace la palabra clave declare en TypeScript?
Le dice al compilador que un valor existe en tiempo de ejecución sin crearlo. declare const VERSION: string; te permite usar VERSION como string, y la línea desaparece de la salida. Si nada define de verdad VERSION, el programa falla en tiempo de ejecución con un ReferenceError.
¿Cómo arreglo "Could not find a declaration file for module"?
El error TS7016 significa que el paquete no tiene tipos. Instálalos si existen (npm i -D @types/package-name), o añade un archivo .d.ts con declare module "package-name" { ... } que describa lo que usas. declare module "package-name"; a secas silencia el error tipando todo el módulo como any.
¿Cómo genero archivos .d.ts desde TypeScript?
Pon "declaration": true en tsconfig.json (o pasa --declaration). Cada archivo .ts produce entonces un .d.ts junto a su .js. Añade emitDeclarationOnly cuando otra herramienta genera el JavaScript, y declarationMap para que los editores puedan saltar de los tipos a tu código fuente.
¿Qué diferencia hay entre declare global y declare module?
declare global { ... } añade cosas al ámbito global, por ejemplo una propiedad nueva en Array o una variable global. declare module "name" { ... } describe un módulo que importas con ese nombre. declare global tiene que estar en un módulo (un archivo con un import o export); declare module declara un módulo nuevo en un archivo sin imports ni exports, y amplía uno existente dentro de un módulo.