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! đ
