TypeScript Declaration Files: The Translator’s Guide 📜
The Analogy: Think of declaration files like a menu at a restaurant. The kitchen (JavaScript library) can cook amazing dishes, but without a menu (declaration file), you don’t know what’s available or how to order. Declaration files tell TypeScript: “Here’s what this library can do!”
What Are Declaration Files?
Imagine you have a friend who speaks only Japanese. You speak only English. You need a translator to talk to each other!
Declaration files (.d.ts) are TypeScript’s translators. They describe what JavaScript code looks like—without running it.
graph TD A["JavaScript Library"] --> B["Declaration File .d.ts"] B --> C["TypeScript Understands!"] C --> D["Autocomplete Works"] C --> E["Type Checking Works"]
Why Do We Need Them?
JavaScript doesn’t have types. TypeScript does. When you use a JavaScript library in TypeScript, TypeScript asks: “What types does this have?”
Declaration files answer that question.
Without declaration file:
// TypeScript is confused 😕
import lodash from 'lodash';
lodash.chunk([1,2,3], 2);
// Error: Cannot find module
With declaration file:
// TypeScript knows everything! 😊
import lodash from 'lodash';
lodash.chunk([1,2,3], 2);
// Returns: [[1,2], [3]]
// Autocomplete works!
Writing Declaration Files
Writing a declaration file is like writing the menu for a restaurant. You describe what’s available without cooking anything.
The Basic Recipe
Declaration files use the declare keyword. It tells TypeScript: “Trust me, this exists somewhere!”
Step 1: Declare a Variable
// myLib.d.ts
declare const VERSION: string;
Step 2: Declare a Function
// myLib.d.ts
declare function greet(name: string): string;
Step 3: Declare a Class
// myLib.d.ts
declare class Calculator {
add(a: number, b: number): number;
subtract(a: number, b: number): number;
}
Declaring a Module
When you want to describe an entire library:
// lodash.d.ts
declare module 'lodash' {
export function chunk<T>(
array: T[],
size: number
): T[][];
export function compact<T>(
array: T[]
): T[];
}
Now TypeScript knows exactly what lodash offers!
Real Example: A Color Library
Imagine a JavaScript library called colorful:
// colorful.js (JavaScript)
function mix(color1, color2) { /*...*/ }
function lighten(color, amount) { /*...*/ }
const PRIMARY = '#3498db';
Here’s its declaration file:
// colorful.d.ts
declare module 'colorful' {
export function mix(
color1: string,
color2: string
): string;
export function lighten(
color: string,
amount: number
): string;
export const PRIMARY: string;
}
DefinitelyTyped and @types
Here’s the magical part: you rarely need to write declaration files yourself!
The Community Library
DefinitelyTyped is like a giant cookbook where thousands of developers share their “menus” (declaration files).
graph TD A["You need types for React"] --> B["Check DefinitelyTyped"] B --> C["Install @types/react"] C --> D["Types work instantly!"]
How to Use @types
It’s incredibly simple:
# Need types for lodash?
npm install @types/lodash
# Need types for express?
npm install @types/express
# Need types for jest?
npm install @types/jest
That’s it! TypeScript automatically finds these types.
How It Works
- You install
@types/lodash - TypeScript looks in
node_modules/@types/ - Finds
lodash/index.d.ts - Now it understands lodash!
What If Types Don’t Exist?
Sometimes a library is too new or obscure. You have three options:
- Write your own (we learned this!)
- Create a quick placeholder:
// types/obscure-lib.d.ts
declare module 'obscure-lib';
- Contribute to DefinitelyTyped (be a hero!)
Ambient Declarations
Ambient declarations describe things that exist in your environment but weren’t imported.
Think of them as telling TypeScript about the furniture in a room it can’t see.
Common Use Cases
Global Variables from a CDN:
<!-- Your HTML loads jQuery from CDN -->
<script src="jquery.min.js"></script>
// Tell TypeScript jQuery exists globally
declare var $: JQueryStatic;
declare var jQuery: JQueryStatic;
Browser APIs Not Yet in TypeScript:
// New browser feature TypeScript
// doesn't know about yet
declare var someNewBrowserAPI: {
doSomething(): void;
version: string;
};
Environment Variables:
// Describe your environment
declare namespace NodeJS {
interface ProcessEnv {
NODE_ENV: 'development' | 'production';
API_KEY: string;
DATABASE_URL: string;
}
}
// Now this is type-safe!
process.env.API_KEY; // string
process.env.FAKE_KEY; // Error!
The declare global Pattern
When you need to add things to the global scope from a module:
// myTypes.ts
export {}; // Makes this a module
declare global {
interface Window {
myApp: {
version: string;
init(): void;
};
}
}
// Now anywhere in your code:
window.myApp.version; // Works!
Module Augmentation
Sometimes a library is almost perfect, but missing something. Module augmentation lets you add to it!
It’s like adding your own special item to a restaurant’s menu.
The Pattern
// Augmenting Express
import { Request } from 'express';
declare module 'express' {
interface Request {
user?: {
id: string;
name: string;
};
}
}
// Now you can use:
app.get('/', (req, res) => {
console.log(req.user?.name);
// TypeScript knows about user!
});
Real-World Example: Adding Methods
// Original library has:
// Array<T> with push, pop, etc.
// You want to add a custom method
declare global {
interface Array<T> {
first(): T | undefined;
last(): T | undefined;
}
}
// Implementation (in a .ts file)
Array.prototype.first = function() {
return this[0];
};
Array.prototype.last = function() {
return this[this.length - 1];
};
// Now this works everywhere!
const nums = [1, 2, 3];
nums.first(); // 1
nums.last(); // 3
graph TD A["Original Module"] --> B["Your Augmentation"] B --> C["Enhanced Module"] C --> D["New methods available"] C --> E["New properties available"]
Key Rules
- Import the original module first
- Use
declare module 'module-name' - Merge interfaces, don’t replace them
Global Augmentation
Global augmentation is like module augmentation, but for the global scope. You’re adding things that work everywhere without importing.
When to Use It
- Adding to
Window - Adding to
globalThis - Extending built-in types like
StringorArray - Adding Node.js globals
The Pattern
// Must export something to be a module
export {};
declare global {
// Add to Window
interface Window {
analytics: {
track(event: string): void;
};
}
// Add a global function
function formatMoney(amount: number): string;
// Add a global variable
const APP_VERSION: string;
}
Practical Example: String Extensions
export {};
declare global {
interface String {
toTitleCase(): string;
truncate(length: number): string;
}
}
// Implementation
String.prototype.toTitleCase = function() {
return this.replace(/\w\S*/g, txt =>
txt.charAt(0).toUpperCase() +
txt.substr(1).toLowerCase()
);
};
String.prototype.truncate = function(len) {
return this.length > len
? this.slice(0, len) + '...'
: this.toString();
};
// Works everywhere now!
"hello world".toTitleCase(); // "Hello World"
"A long string".truncate(5); // "A lon..."
The Secret Sauce: export {}
Why do we need export {}?
In TypeScript, a file with no imports/exports is a script (global scope). Adding export {} makes it a module, which is required for declare global to work.
graph TD A["File with no exports"] --> B["Script mode"] B --> C["declare global not allowed"] D["File with export"] --> E["Module mode"] E --> F["declare global works!"]
Quick Summary
| Concept | What It Does | When to Use |
|---|---|---|
| Declaration Files | Describe JS code types | Using untyped libraries |
| @types packages | Ready-made declarations | Popular libraries |
| Ambient Declarations | Describe global things | CDN scripts, env vars |
| Module Augmentation | Add to existing modules | Extending libraries |
| Global Augmentation | Add to global scope | Window, built-in types |
Your Declaration File Toolkit
// 1. Quick placeholder for any module
declare module 'some-lib';
// 2. Describe a module's exports
declare module 'some-lib' {
export function doThing(): void;
}
// 3. Add to existing module
declare module 'express' {
interface Request {
custom: string;
}
}
// 4. Add to global scope
declare global {
interface Window {
myThing: any;
}
}
Remember: Declaration files are just descriptions. They don’t create code—they describe code that already exists somewhere. They’re the bridge between JavaScript’s flexibility and TypeScript’s safety!
Now you can make TypeScript understand any JavaScript code in the world. You’re a translator between two languages! 🌍
