Module augmentation extends existing types without modifying source code. Here's how to use it.
Basic Module Augmentation#
1// Extending a module's types
2// types/express.d.ts
3import 'express';
4
5declare module 'express' {
6 interface Request {
7 user?: {
8 id: string;
9 email: string;
10 role: string;
11 };
12 requestId: string;
13 }
14}
15
16// Now Request has user and requestId
17import { Request, Response } from 'express';
18
19app.use((req: Request, res: Response, next) => {
20 req.requestId = generateId();
21 next();
22});
23
24app.get('/profile', (req: Request, res: Response) => {
25 if (req.user) {
26 res.json({ id: req.user.id, email: req.user.email });
27 }
28});Global Augmentation#
1// Extend global types
2// types/global.d.ts
3declare global {
4 interface Window {
5 analytics: {
6 track: (event: string, data?: object) => void;
7 identify: (userId: string) => void;
8 };
9 config: {
10 apiUrl: string;
11 environment: 'development' | 'staging' | 'production';
12 };
13 }
14
15 // Extend built-in types
16 interface Array<T> {
17 customMethod(): T[];
18 }
19
20 // Add global variables
21 var DEBUG: boolean;
22 var VERSION: string;
23}
24
25export {}; // Make this a module
26
27// Usage
28window.analytics.track('page_view', { path: '/home' });
29console.log(window.config.environment);
30
31if (DEBUG) {
32 console.log('Debug mode enabled');
33}Declaration Merging#
1// Interface merging
2interface User {
3 id: string;
4 name: string;
5}
6
7interface User {
8 email: string;
9 createdAt: Date;
10}
11
12// Merged interface has all properties
13const user: User = {
14 id: '1',
15 name: 'John',
16 email: 'john@example.com',
17 createdAt: new Date(),
18};
19
20// Namespace merging with function
21function greet(name: string): string {
22 return `Hello, ${name}`;
23}
24
25namespace greet {
26 export function formal(name: string): string {
27 return `Good day, ${name}`;
28 }
29
30 export const version = '1.0.0';
31}
32
33greet('John'); // "Hello, John"
34greet.formal('John'); // "Good day, John"
35greet.version; // "1.0.0"
36
37// Class merging with namespace
38class Album {
39 constructor(public name: string) {}
40}
41
42namespace Album {
43 export interface Track {
44 name: string;
45 duration: number;
46 }
47
48 export function create(name: string): Album {
49 return new Album(name);
50 }
51}
52
53const album = Album.create('Greatest Hits');
54const track: Album.Track = { name: 'Song 1', duration: 180 };Extending Third-Party Libraries#
1// Extend React types
2// types/react.d.ts
3import 'react';
4
5declare module 'react' {
6 interface CSSProperties {
7 // Add custom CSS properties
8 '--primary-color'?: string;
9 '--spacing'?: string;
10 }
11}
12
13// Usage
14const style: React.CSSProperties = {
15 color: 'blue',
16 '--primary-color': '#007bff',
17 '--spacing': '1rem',
18};
19
20// Extend axios
21// types/axios.d.ts
22import 'axios';
23
24declare module 'axios' {
25 interface AxiosRequestConfig {
26 skipAuth?: boolean;
27 retryCount?: number;
28 }
29}
30
31// Usage
32axios.get('/api/public', { skipAuth: true });
33
34// Extend Prisma
35// types/prisma.d.ts
36import { User as PrismaUser } from '@prisma/client';
37
38declare global {
39 namespace PrismaJson {
40 type UserMetadata = {
41 theme: 'light' | 'dark';
42 notifications: boolean;
43 };
44 }
45}
46
47// Extend Next.js
48// types/next.d.ts
49import 'next';
50
51declare module 'next' {
52 interface NextApiRequest {
53 user?: {
54 id: string;
55 role: string;
56 };
57 }
58}Environment Variables#
1// types/env.d.ts
2declare global {
3 namespace NodeJS {
4 interface ProcessEnv {
5 NODE_ENV: 'development' | 'production' | 'test';
6 DATABASE_URL: string;
7 API_KEY: string;
8 PORT?: string;
9 REDIS_URL?: string;
10 }
11 }
12}
13
14export {};
15
16// Now process.env is typed
17const dbUrl: string = process.env.DATABASE_URL; // OK
18const port: string | undefined = process.env.PORT; // OK
19const env: 'development' | 'production' | 'test' = process.env.NODE_ENV;
20
21// Vite environment variables
22// types/vite-env.d.ts
23/// <reference types="vite/client" />
24
25interface ImportMetaEnv {
26 readonly VITE_API_URL: string;
27 readonly VITE_APP_TITLE: string;
28 readonly VITE_ENABLE_ANALYTICS: string;
29}
30
31interface ImportMeta {
32 readonly env: ImportMetaEnv;
33}Extending Built-in Types#
1// Extend String
2declare global {
3 interface String {
4 toTitleCase(): string;
5 truncate(length: number): string;
6 }
7}
8
9String.prototype.toTitleCase = function() {
10 return this.replace(/\w\S*/g, (txt) =>
11 txt.charAt(0).toUpperCase() + txt.slice(1).toLowerCase()
12 );
13};
14
15String.prototype.truncate = function(length: number) {
16 return this.length > length ? this.slice(0, length) + '...' : this.toString();
17};
18
19// Usage
20'hello world'.toTitleCase(); // "Hello World"
21'Long text here'.truncate(8); // "Long tex..."
22
23// Extend Array
24declare global {
25 interface Array<T> {
26 unique(): T[];
27 groupBy<K extends keyof T>(key: K): Record<T[K] & string, T[]>;
28 chunk(size: number): T[][];
29 }
30}
31
32Array.prototype.unique = function<T>(): T[] {
33 return [...new Set(this)];
34};
35
36Array.prototype.groupBy = function<T, K extends keyof T>(key: K) {
37 return this.reduce((acc, item) => {
38 const groupKey = item[key] as string;
39 acc[groupKey] = acc[groupKey] || [];
40 acc[groupKey].push(item);
41 return acc;
42 }, {} as Record<string, T[]>);
43};
44
45// Usage
46[1, 2, 2, 3].unique(); // [1, 2, 3]
47users.groupBy('role'); // { admin: [...], user: [...] }Library Type Definitions#
1// When library lacks types
2// types/untyped-library.d.ts
3declare module 'untyped-library' {
4 export interface Config {
5 debug?: boolean;
6 timeout?: number;
7 }
8
9 export function initialize(config: Config): void;
10 export function process(data: unknown): Promise<Result>;
11
12 export interface Result {
13 success: boolean;
14 data?: unknown;
15 error?: string;
16 }
17
18 export default class Client {
19 constructor(apiKey: string);
20 send(message: string): Promise<void>;
21 on(event: 'message', callback: (msg: string) => void): void;
22 on(event: 'error', callback: (err: Error) => void): void;
23 }
24}
25
26// Wildcard module declaration
27declare module '*.svg' {
28 const content: React.FunctionComponent<React.SVGAttributes<SVGElement>>;
29 export default content;
30}
31
32declare module '*.css' {
33 const classes: { [key: string]: string };
34 export default classes;
35}
36
37declare module '*.json' {
38 const value: unknown;
39 export default value;
40}Path Aliases#
1// tsconfig.json
2{
3 "compilerOptions": {
4 "baseUrl": ".",
5 "paths": {
6 "@/*": ["src/*"],
7 "@components/*": ["src/components/*"],
8 "@utils/*": ["src/utils/*"]
9 }
10 }
11}
12
13// types/paths.d.ts - For non-TypeScript files
14declare module '@assets/*' {
15 const src: string;
16 export default src;
17}
18
19declare module '@styles/*' {
20 const classes: { readonly [key: string]: string };
21 export default classes;
22}Testing Augmentation#
1// types/jest.d.ts
2import '@testing-library/jest-dom';
3
4declare global {
5 namespace jest {
6 interface Matchers<R> {
7 toBeWithinRange(floor: number, ceiling: number): R;
8 toHaveBeenCalledOnceWith(...args: unknown[]): R;
9 }
10 }
11}
12
13// Custom matcher implementation
14expect.extend({
15 toBeWithinRange(received, floor, ceiling) {
16 const pass = received >= floor && received <= ceiling;
17 return {
18 pass,
19 message: () =>
20 `expected ${received} to be within range ${floor} - ${ceiling}`,
21 };
22 },
23});
24
25// Usage
26expect(100).toBeWithinRange(90, 110);Best Practices#
Organization:
✓ Put declarations in types/ directory
✓ Name files descriptively (express.d.ts)
✓ Include in tsconfig.json
✓ Export {} to make modules
Safety:
✓ Don't override existing types carelessly
✓ Use optional properties when uncertain
✓ Document augmentations
✓ Keep augmentations minimal
Patterns:
✓ Extend Request/Response for auth
✓ Type environment variables
✓ Augment testing matchers
✓ Add global utilities sparingly
Conclusion#
Module augmentation extends existing types without modifying source code. Use it for adding properties to Express requests, typing environment variables, extending third-party libraries, and creating global utilities. Keep augmentations organized and documented for maintainability.