Module augmentation allows you to extend existing modules by adding new declarations. This is essential when working with third-party libraries that have incomplete or missing type definitions.
Module augmentation is TypeScript’s way of saying “I want to add these types to an existing module.” It’s particularly useful for:
// Now we can use the complete typesimport * as dateWizard from 'date-wizard';const registered = new Date('2016-06-01T16:23:13');// Use augmented interface with time fieldsconst details = dateWizard.dateDetails(registered);console.log(details.hours); // ✓ TypeScript knows about hours nowconsole.log(details.minutes); // ✓ TypeScript knows about minutes nowconsole.log(details.seconds); // ✓ TypeScript knows about seconds now// Use augmented function exportconst padded = dateWizard.pad(5); // ✓ TypeScript knows about pad nowconsole.log(padded); // "05"
When to use module augmentation:
Third-party library has incomplete types
You’re using JavaScript libraries with @types packages
You need to add custom properties to library types
// Augment the global scopedeclare global { interface Window { myCustomProperty: string; myCustomFunction(): void; } var MY_GLOBAL_CONSTANT: number;}// Now these are available globallywindow.myCustomProperty = 'Hello';window.myCustomFunction();console.log(MY_GLOBAL_CONSTANT);
// Handle CSS modulesdeclare module '*.css' { const styles: { [key: string]: string }; export default styles;}// Handle image importsdeclare module '*.png' { const content: string; export default content;}// Handle JSON importsdeclare module '*.json' { const value: any; export default value;}// Now you can import these filesimport styles from './App.css';import logo from './logo.png';import config from './config.json';
Why are these called 'ambient' declarations?
They’re called “ambient” because they describe the environment or context in which your code runs, rather than being the actual implementation. They tell TypeScript “trust me, this exists at runtime.”
For larger augmentations, use separate .d.ts files:
// types/express/index.d.tsimport { User } from '../../models/User';declare global { namespace Express { interface Request { user?: User; sessionId?: string; } }}// Now available in all Express routesimport { Request, Response } from 'express';app.get('/profile', (req: Request, res: Response) => { if (req.user) { res.json(req.user); // ✓ TypeScript knows about req.user }});
Interfaces with the same name automatically merge:
// First declarationinterface User { name: string; age: number;}// Second declaration - merges with the firstinterface User { email: string;}// Merged resultconst user: User = { name: 'John', age: 30, email: '[email protected]' // All properties required};
Interfaces Merge
Types Don't Merge
interface User { name: string;}interface User { age: number;}// Merged: { name: string; age: number }
type User = { name: string;};type User = { // ✗ Error: Duplicate identifier age: number;};
/** * Augment date-wizard module to add missing type definitions. * * The library exports a `pad` function but doesn't include it in types. * DateDetails interface is missing time-related fields that exist at runtime. * * @see https://github.com/library/date-wizard/issues/123 */declare module 'date-wizard' { const pad: (num: number) => string; interface DateDetails { hours: number; minutes: number; seconds: number; }}
Consider contributing upstream
If you’re augmenting a popular library:
Check if types already exist in DefinitelyTyped
Consider submitting a PR to the library
Consider submitting to @types packages
Document the augmentation for your team
Temporary augmentation is fine, but contributing helps everyone!
Use import type for type-only imports
When importing only for types, use import type:
import type { User } from './models';declare module 'express' { interface Request { user?: User; }}
This ensures the import is removed during compilation.