19.1 What Declaration Files Are and Why They Exist
Right, let’s talk about the digital equivalent of a restaurant menu: the TypeScript Declaration File, or .d.ts. You’ve written some beautiful, type-safe TypeScript. Then you npm install a library, and… nothing. Your editor lights up like a Christmas tree with Cannot find module 'cool-library' or its corresponding type declarations. What gives?
The library is written in plain JavaScript. It exists, your Node runtime can require it just fine, but the TypeScript compiler has absolutely no idea what’s inside that box. Is it a beautiful porcelain vase? A pile of angry bees? It needs a description. A declaration file is that description. It’s a file full of only type information—no actual logic, no console.logs, just a meticulously typed map of what the JavaScript code looks like. It tells TypeScript, “Hey, inside cool-library, there’s a function called configureBees that takes an AngryBeeOptions object and returns a Promise<Honey>.” TypeScript breathes a sigh of relief and gets back to work. You get autocomplete, IntelliSense, and compile-time safety, all without the library author having to rewrite their entire project in TypeScript. It’s a genius compromise.
The Anatomy of a .d.ts File
Think of a .d.ts file as a TypeScript file where you’ve surgically removed everything that produces a value and left only the parts that describe types. Let’s break down a real-world example. Imagine a simple JS library in beeper.js:
// beeper.js - The actual JavaScript
function beep(count, delay) {
for (let i = 0; i < count; i++) {
setTimeout(() => console.log('Beep!'), delay * i);
}
}
module.exports = { beep };
Its declaration file, beeper.d.ts, would look like this:
// beeper.d.ts - Just the types, ma'am
declare function beep(count: number, delay?: number): void;
export = beeper; // This is the key line for a CommonJS module
Notice what’s not here: there’s no function body. The declare keyword is our signal flare to the TypeScript compiler, shouting “I PROMISE this exists at runtime! Just trust me on the types!”. The ?: for delay makes it optional, which is a crucial bit of info the JS code alone could never convey.
declare and Ambient Context
The declare keyword is the workhorse of declaration files. It creates what’s called an “ambient” declaration—something that exists in the surrounding environment but isn’t defined in the current TypeScript file. You’re announcing its presence to the type system.
This is also how we describe things that are global, like the window object. The TypeScript compiler itself provides a massive lib.d.ts file full of declarations for all standard JavaScript and DOM APIs. You’ve been using .d.ts files since your first document.getElementById. Mind-blown, right?
The Two Flavors: Global and Module
This is where people often faceplant. Declaration files come in two styles, and using the wrong one will break everything in wonderfully confusing ways.
Module Declarations describe a library you import. Their top-level declarations are automatically scoped to the module. You must use an export statement to make them available.
// For a module like 'cool-library'
declare module 'cool-library' {
export function configureBees(options: Options): Promise<Honey>;
export interface Options { ... }
}
Global Declarations describe things that are available in the global scope (like window). These files must not have any top-level import or export statements, because that would turn them into a module and scoped the declarations.
// For something you'd access on 'window'
declare namespace MyGlobalLib {
function doSomethingCool(): void;
}
// This tells TS that on the global `window` object, there will be a `myGlobalLib` property.
interface Window {
myGlobalLib: MyGlobalLib;
}
Mixing these up is a classic “why isn’t this working?!” moment. If your library is imported, its types must be exported from a module declaration. If it’s a global, your file must have no imports/exports.
Reading the Tea Leaves: How It All Fits Together
So you install @types/cool-library. How does TypeScript even find it? The process is straightforward but specific:
- Core Type Definitions: First, it uses its built-in
lib.*.d.tsfiles for standard JS. node_modules/@types/: When you try to importfrom 'cool-library', it looks for a package named@types/cool-libraryin yournode_modules. This is the preferred home for DefinitelyTyped packages.- Bundled Types: Many modern libraries bundle their own definitions. The
package.jsonhas a"types"or"typings"field (they’re synonyms, because of course we needed two names for the same thing) pointing to the.d.tsfile, e.g.,"types": "dist/index.d.ts". TypeScript checks this first. This is the best practice—it keeps the code and its types in sync.
When You Gotta Roll Your Own
Sometimes, you’ll find a library that has no types. The horror! Don’t panic. Your first move should be to check DefinitelyTyped (npm install -D @types/library-name). If it’s not there, you have two options:
Quick and Dirty Ambient Module Declaration: Create a
types/folder and add adeclarations.d.tsfile. In it, you can shut TypeScript up for a whole module:// types/declarations.d.ts declare module 'obscure-js-library' { const content: any; export default content; }This is a starting point. You’re telling TypeScript, “I don’t know what this is, but it exists, so let me import it as
any.” It’s better than nothing, but it’s a type-safe desert.The Right Way: Writing a Full Declaration File: For anything non-trivial, you’ll want to properly type it. This is an excellent way to really learn how a library works. You create a file (e.g.,
obscure-js-library.d.ts) and start declaring its modules, functions, and interfaces based on its documentation or source code. It’s tedious but incredibly rewarding. You’ll feel like a type wizard when you’re done. And hey, you can always submit it to DefinitelyTyped afterward and become a hero to developers everywhere.