Skip to main content

What is Module Augmentation?

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:
  • Extending third-party library types
  • Adding custom methods to built-in objects
  • Fixing incomplete type definitions
  • Declaring global variables

Basic Module Augmentation

From Exercise 13, here’s how to augment a module:
// date-wizard library has incomplete types
import 'date-wizard';

// Augment the module to add missing declarations
declare module 'date-wizard' {
    // Add missing function export
    const pad: (num: number) => string;

    // Extend incomplete interface
    interface DateDetails {
        hours: number;
        minutes: number;
        seconds: number;
    }
}

Original Library Code

The original date-wizard module had incomplete types:
// node_modules/date-wizard/index.d.ts (incomplete)
declare module 'date-wizard' {
    interface DateDetails {
        year: number;
        month: number;
        date: number;
        // Missing: hours, minutes, seconds
    }

    function dateWizard(date: Date, format: string): string;
    function dateDetails(date: Date): DateDetails;

    // Missing: pad function export
}

After Augmentation

// Now we can use the complete types
import * as dateWizard from 'date-wizard';

const registered = new Date('2016-06-01T16:23:13');

// Use augmented interface with time fields
const details = dateWizard.dateDetails(registered);
console.log(details.hours);    // ✓ TypeScript knows about hours now
console.log(details.minutes);  // ✓ TypeScript knows about minutes now
console.log(details.seconds);  // ✓ TypeScript knows about seconds now

// Use augmented function export
const padded = dateWizard.pad(5);  // ✓ TypeScript knows about pad now
console.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
  • You’re waiting for official type updates

Augmenting Global Scope

You can also augment the global namespace:
// Augment the global scope
declare global {
    interface Window {
        myCustomProperty: string;
        myCustomFunction(): void;
    }

    var MY_GLOBAL_CONSTANT: number;
}

// Now these are available globally
window.myCustomProperty = 'Hello';
window.myCustomFunction();
console.log(MY_GLOBAL_CONSTANT);

Example: Extending Node.js Process

declare global {
    namespace NodeJS {
        interface ProcessEnv {
            DATABASE_URL: string;
            API_KEY: string;
            NODE_ENV: 'development' | 'production' | 'test';
        }
    }
}

// Now process.env is properly typed
const dbUrl: string = process.env.DATABASE_URL;
const nodeEnv: 'development' | 'production' | 'test' = process.env.NODE_ENV;
Be careful with global augmentation! It affects the entire project and can cause conflicts. Use it sparingly and document your changes.

Ambient Declarations

Ambient declarations describe the shape of code that exists elsewhere (usually in JavaScript).

Declaring Modules

When a library has no types at all:
// declarations.d.ts
declare module 'some-untyped-library' {
    export function doSomething(value: string): number;
    export interface Config {
        timeout: number;
        retries: number;
    }
}
Now you can import and use it with types:
import { doSomething, Config } from 'some-untyped-library';

const result = doSomething('test');  // number
const config: Config = { timeout: 1000, retries: 3 };

Declaring Global Variables

// For variables injected by build tools or script tags
declare const API_URL: string;
declare const VERSION: string;

// Usage
console.log(`API: ${API_URL}, Version: ${VERSION}`);

Wildcard Module Declarations

For non-JavaScript files:
// Handle CSS modules
declare module '*.css' {
    const styles: { [key: string]: string };
    export default styles;
}

// Handle image imports
declare module '*.png' {
    const content: string;
    export default content;
}

// Handle JSON imports
declare module '*.json' {
    const value: any;
    export default value;
}

// Now you can import these files
import styles from './App.css';
import logo from './logo.png';
import config from './config.json';
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.”

Practical Patterns

Extending Third-Party Libraries

From Exercise 13, here’s a complete example:
// File: module-augmentations/date-wizard/index.ts
import 'date-wizard';

declare module 'date-wizard' {
    // Add missing export
    const pad: (num: number) => string;

    // Extend existing interface
    interface DateDetails {
        hours: number;
        minutes: number;
        seconds: number;
    }
}

// File: main.ts
import * as dateWizard from 'date-wizard';
import './module-augmentations/date-wizard';

interface Person {
    name: string;
    age: number;
    registered: Date;
}

const persons: Person[] = [
    {
        name: 'Max Mustermann',
        age: 25,
        registered: new Date('2016-02-15T09:25:13')
    },
    {
        name: 'Kate Müller',
        age: 23,
        registered: new Date('2016-03-23T12:47:03')
    },
];

function logPerson(person: Person, index: number) {
    // Use augmented types
    const registeredAt = dateWizard(person.registered, '{date}.{month}.{year} {hours}:{minutes}');
    const num = `#${dateWizard.pad(index + 1)}`;
    console.log(`${num}: ${person.name}, ${person.age}, ${registeredAt}`);
}

console.log('Early birds:');
persons
    .filter((person) => dateWizard.dateDetails(person.registered).hours < 10)
    .forEach(logPerson);

Creating Type Definition Files

For larger augmentations, use separate .d.ts files:
// types/express/index.d.ts
import { User } from '../../models/User';

declare global {
    namespace Express {
        interface Request {
            user?: User;
            sessionId?: string;
        }
    }
}

// Now available in all Express routes
import { Request, Response } from 'express';

app.get('/profile', (req: Request, res: Response) => {
    if (req.user) {
        res.json(req.user);  // ✓ TypeScript knows about req.user
    }
});

Augmenting Built-in Types

// Extend Array with custom methods
declare global {
    interface Array<T> {
        first(): T | undefined;
        last(): T | undefined;
    }
}

// Implementation (in a separate file)
Array.prototype.first = function() {
    return this[0];
};

Array.prototype.last = function() {
    return this[this.length - 1];
};

// Usage
const numbers = [1, 2, 3, 4, 5];
console.log(numbers.first());  // 1
console.log(numbers.last());   // 5
Extending built-in prototypes is generally discouraged because:
  • It can conflict with future JavaScript features
  • It affects all code in your project
  • It makes code harder to understand
  • It can cause issues in third-party libraries
Use utility functions instead when possible.

Declaration Merging

Interfaces with the same name automatically merge:
// First declaration
interface User {
    name: string;
    age: number;
}

// Second declaration - merges with the first
interface User {
    email: string;
}

// Merged result
const user: User = {
    name: 'John',
    age: 30,
    email: '[email protected]'  // All properties required
};
interface User {
    name: string;
}

interface User {
    age: number;
}

// Merged: { name: string; age: number }

Best Practices

Organize module augmentations in a dedicated directory:
src/
├── module-augmentations/
│   ├── date-wizard/
│   │   └── index.ts
│   ├── express/
│   │   └── index.d.ts
│   └── global/
│       └── index.d.ts
└── index.ts
Add comments explaining the augmentation:
/**
 * 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;
    }
}
If you’re augmenting a popular library:
  1. Check if types already exist in DefinitelyTyped
  2. Consider submitting a PR to the library
  3. Consider submitting to @types packages
  4. Document the augmentation for your team
Temporary augmentation is fine, but contributing helps everyone!
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.

Common Patterns

Pattern 1: Plugin System Types

// core.ts
export interface PluginAPI {
    version: string;
}

// plugin-system.d.ts
import { PluginAPI } from './core';

declare module './core' {
    interface PluginAPI {
        // Plugins can extend this
        registerPlugin(name: string, plugin: unknown): void;
    }
}

// my-plugin.ts
import { PluginAPI } from './core';

declare module './core' {
    interface PluginAPI {
        myPluginMethod(): void;
    }
}

Pattern 2: Framework Extensions

// Extend Vue component options
declare module 'vue/types/options' {
    interface ComponentOptions<V> {
        myCustomOption?: string;
    }
}

// Extend Next.js page props
declare module 'next' {
    interface NextPageContext {
        customProperty: string;
    }
}

Pattern 3: Environment Variables

declare global {
    namespace NodeJS {
        interface ProcessEnv {
            NODE_ENV: 'development' | 'production' | 'test';
            DATABASE_URL: string;
            API_KEY: string;
            PORT: string;
        }
    }
}

// Now fully typed
const dbUrl = process.env.DATABASE_URL;  // string
const port = parseInt(process.env.PORT); // string -> number

Interfaces

Understand interface declaration merging

Type Guards

Use type guards with augmented types

Exercise 13

Practice module augmentation with date-wizard

Generics

Combine generics with module augmentation

Further Reading

Build docs developers (and LLMs) love