Back to Blog
TypeScriptModule AugmentationTypesAdvanced

TypeScript Module Augmentation

Extend existing types in TypeScript. From declaration merging to global augmentation to library extensions.

B
Bootspring Team
Engineering
June 29, 2021
6 min read

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.

Share this article

Help spread the word about Bootspring