Module augmentation and declaration merging are advanced TypeScript features that allow extending existing modules and combining declarations. These mechanisms are crucial when working with external libraries, creating plugins, and extending framework functionality.
1// Interface merging - the most common case
2interface User {
3 id: number;
4 name: string;
5}
6
7interface User {
8 email: string; // Adding a new property
9 age?: number; // And another optional one
10}
11
12// TypeScript automatically merges both declarations
13const user: User = {
14 id: 1,
15 name: "John Smith",
16 email: "john@example.com"
17 // age is optional
18};
19
20// Namespace merging
21namespace MyApp {
22 export interface Config {
23 apiUrl: string;
24 }
25
26 export function initialize(config: Config): void {
27 console.log("Initializing with:", config.apiUrl);
28 }
29}
30
31namespace MyApp {
32 export interface Config {
33 timeout?: number; // Extending the Config interface
34 }
35
36 export class Logger {
37 log(message: string): void {
38 console.log(`[MyApp]: ${message}`);
39 }
40 }
41}
42
43// Using merged declarations
44const config: MyApp.Config = {
45 apiUrl: "https://api.example.com",
46 timeout: 5000
47};
48
49const logger = new MyApp.Logger();
50logger.log("Application started");
51
52// Class and namespace merging
53class Album {
54 constructor(public title: string, public artist: string) {}
55}
56
57namespace Album {
58 export interface Track {
59 number: number;
60 title: string;
61 duration: number;
62 }
63
64 export function createFromJSON(json: string): Album {
65 const data = JSON.parse(json);
66 return new Album(data.title, data.artist);
67 }
68}
69
70// Using class and namespace together
71const album = new Album("The Wall", "Pink Floyd");
72const track: Album.Track = {
73 number: 1,
74 title: "In The Flesh?",
75 duration: 196
76};
77
78const albumFromJSON = Album.createFromJSON('{"title": "Dark Side", "artist": "Pink Floyd"}');1// Function and namespace merging - useful for creating callable objects
2function createCounter(initial: number = 0): () => number {
3 let count = initial;
4 return () => ++count;
5}
6
7namespace createCounter {
8 export interface Options {
9 initial?: number;
10 step?: number;
11 max?: number;
12 }
13
14 export function withOptions(options: Options): () => number {
15 let count = options.initial || 0;
16 const step = options.step || 1;
17 const max = options.max || Infinity;
18
19 return () => {
20 if (count + step > max) {
21 return count;
22 }
23 count += step;
24 return count;
25 };
26 }
27
28 export function reset(counter: () => number): void {
29 // Reset implementation
30 console.log("Counter reset");
31 }
32}
33
34// Using the function and its namespace
35const counter1 = createCounter(10);
36const counter2 = createCounter.withOptions({ initial: 0, step: 5, max: 100 });
37
38console.log(counter1()); // 11
39console.log(counter2()); // 5
40createCounter.reset(counter1);
41
42// Enum and namespace merging
43enum Status {
44 Idle = "IDLE",
45 Loading = "LOADING",
46 Success = "SUCCESS",
47 Error = "ERROR"
48}
49
50namespace Status {
51 export function isComplete(status: Status): boolean {
52 return status === Status.Success || status === Status.Error;
53 }
54
55 export function isInProgress(status: Status): boolean {
56 return status === Status.Loading;
57 }
58
59 export interface Metadata {
60 timestamp: Date;
61 message?: string;
62 }
63}
64
65// Using enum with additional functions
66const currentStatus = Status.Loading;
67console.log(Status.isInProgress(currentStatus)); // true
68console.log(Status.isComplete(currentStatus)); // false1// Generic interface merging
2interface Collection<T> {
3 items: T[];
4 add(item: T): void;
5}
6
7interface Collection<T> {
8 remove(item: T): boolean;
9 find(predicate: (item: T) => boolean): T | undefined;
10}
11
12class ArrayCollection<T> implements Collection<T> {
13 items: T[] = [];
14
15 add(item: T): void {
16 this.items.push(item);
17 }
18
19 remove(item: T): boolean {
20 const index = this.items.indexOf(item);
21 if (index > -1) {
22 this.items.splice(index, 1);
23 return true;
24 }
25 return false;
26 }
27
28 find(predicate: (item: T) => boolean): T | undefined {
29 return this.items.find(predicate);
30 }
31}
32
33// Generic namespace merging
34namespace DataStructures {
35 export interface Node<T> {
36 value: T;
37 next?: Node<T>;
38 }
39
40 export class LinkedList<T> {
41 head?: Node<T>;
42
43 add(value: T): void {
44 const node: Node<T> = { value };
45 if (!this.head) {
46 this.head = node;
47 } else {
48 let current = this.head;
49 while (current.next) {
50 current = current.next;
51 }
52 current.next = node;
53 }
54 }
55 }
56}
57
58namespace DataStructures {
59 export interface Node<T> {
60 prev?: Node<T>; // Adding pointer to previous element
61 }
62
63 export class DoublyLinkedList<T> extends LinkedList<T> {
64 // Doubly linked list implementation
65 }
66}1// Extending the Express module
2declare module "express" {
3 interface Request {
4 user?: {
5 id: string;
6 email: string;
7 roles: string[];
8 };
9 requestId?: string;
10 }
11
12 interface Response {
13 sendSuccess(data: any): void;
14 sendError(error: Error, statusCode?: number): void;
15 }
16}
17
18// Extension implementation
19import express from "express";
20
21const app = express();
22
23// Middleware adding user and requestId
24app.use((req, res, next) => {
25 req.requestId = crypto.randomUUID();
26 // Authentication and adding user
27 req.user = {
28 id: "123",
29 email: "user@example.com",
30 roles: ["user", "admin"]
31 };
32 next();
33});
34
35// Extending Response with new methods
36app.use((req, res, next) => {
37 res.sendSuccess = function(data: any) {
38 this.json({
39 success: true,
40 data,
41 requestId: req.requestId
42 });
43 };
44
45 res.sendError = function(error: Error, statusCode: number = 500) {
46 this.status(statusCode).json({
47 success: false,
48 error: error.message,
49 requestId: req.requestId
50 });
51 };
52
53 next();
54});
55
56// Using extended types
57app.get("/profile", (req, res) => {
58 if (!req.user) {
59 return res.sendError(new Error("Unauthorized"), 401);
60 }
61
62 res.sendSuccess({
63 user: req.user,
64 requestId: req.requestId
65 });
66});1// Extending global objects
2declare global {
3 interface Window {
4 myApp: {
5 version: string;
6 config: {
7 apiUrl: string;
8 debugMode: boolean;
9 };
10 utils: {
11 formatDate(date: Date): string;
12 parseJSON<T>(json: string): T | null;
13 };
14 };
15 }
16
17 interface Array<T> {
18 groupBy<K extends keyof T>(key: K): Record<string, T[]>;
19 chunk(size: number): T[][];
20 }
21
22 interface String {
23 capitalize(): string;
24 toSlug(): string;
25 }
26}
27
28// Implementing Array extensions
29Array.prototype.groupBy = function<T, K extends keyof T>(this: T[], key: K): Record<string, T[]> {
30 return this.reduce((acc, item) => {
31 const groupKey = String(item[key]);
32 if (!acc[groupKey]) {
33 acc[groupKey] = [];
34 }
35 acc[groupKey].push(item);
36 return acc;
37 }, {} as Record<string, T[]>);
38};
39
40Array.prototype.chunk = function<T>(this: T[], size: number): T[][] {
41 const chunks: T[][] = [];
42 for (let i = 0; i < this.length; i += size) {
43 chunks.push(this.slice(i, i + size));
44 }
45 return chunks;
46};
47
48// Implementing String extensions
49String.prototype.capitalize = function(this: string): string {
50 return this.charAt(0).toUpperCase() + this.slice(1).toLowerCase();
51};
52
53String.prototype.toSlug = function(this: string): string {
54 return this
55 .toLowerCase()
56 .trim()
57 .replace(/[^ws-]/g, '')
58 .replace(/[s_-]+/g, '-')
59 .replace(/^-+|-+$/g, '');
60};
61
62// Using extensions
63const users = [
64 { name: "Jan", age: 30, department: "IT" },
65 { name: "Anna", age: 25, department: "HR" },
66 { name: "Piotr", age: 35, department: "IT" }
67];
68
69const grouped = users.groupBy("department");
70console.log(grouped); // { IT: [...], HR: [...] }
71
72const chunks = [1, 2, 3, 4, 5, 6, 7, 8].chunk(3);
73console.log(chunks); // [[1, 2, 3], [4, 5, 6], [7, 8]]
74
75const title = "hello world from typescript".capitalize();
76console.log(title); // "Hello world from typescript"
77
78const slug = "Hello World! From TypeScript".toSlug();
79console.log(slug); // "hello-world-from-typescript"
80
81// Window extensions
82window.myApp = {
83 version: "1.0.0",
84 config: {
85 apiUrl: "https://api.example.com",
86 debugMode: true
87 },
88 utils: {
89 formatDate(date: Date): string {
90 return date.toLocaleDateString("en-US");
91 },
92 parseJSON<T>(json: string): T | null {
93 try {
94 return JSON.parse(json) as T;
95 } catch {
96 return null;
97 }
98 }
99 }
100};1// Base plugin system
2interface PluginSystem {
3 plugins: Map<string, Plugin>;
4 register(plugin: Plugin): void;
5 execute(hookName: string, context: any): void;
6}
7
8interface Plugin {
9 name: string;
10 version: string;
11 hooks?: {
12 [hookName: string]: (context: any) => void | Promise<void>;
13 };
14}
15
16class PluginManager implements PluginSystem {
17 plugins = new Map<string, Plugin>();
18
19 register(plugin: Plugin): void {
20 this.plugins.set(plugin.name, plugin);
21 console.log(`Plugin ${plugin.name} v${plugin.version} registered`);
22 }
23
24 async execute(hookName: string, context: any): Promise<void> {
25 for (const [name, plugin] of this.plugins) {
26 if (plugin.hooks && plugin.hooks[hookName]) {
27 await plugin.hooks[hookName](context);
28 }
29 }
30 }
31}
32
33// Extending the plugin system through different modules
34declare module "./plugin-system" {
35 interface PluginContext {
36 user?: {
37 id: string;
38 name: string;
39 };
40 request?: {
41 url: string;
42 method: string;
43 };
44 }
45
46 interface PluginHooks {
47 beforeRequest?: (context: PluginContext) => void;
48 afterRequest?: (context: PluginContext & { response: any }) => void;
49 onError?: (context: PluginContext & { error: Error }) => void;
50 }
51}
52
53// Authentication plugin
54const authPlugin: Plugin = {
55 name: "auth",
56 version: "1.0.0",
57 hooks: {
58 beforeRequest(context) {
59 console.log("Checking authentication...");
60 if (!context.user) {
61 throw new Error("Not authenticated");
62 }
63 }
64 }
65};
66
67// Logging plugin
68const loggingPlugin: Plugin = {
69 name: "logging",
70 version: "1.0.0",
71 hooks: {
72 beforeRequest(context) {
73 console.log(`[${new Date().toISOString()}] ${context.request?.method} ${context.request?.url}`);
74 },
75 afterRequest(context) {
76 console.log(`Response for ${context.request?.url}:`, context.response);
77 },
78 onError(context) {
79 console.error(`Error for ${context.request?.url}:`, context.error);
80 }
81 }
82};
83
84// Using the plugin system
85const pluginManager = new PluginManager();
86pluginManager.register(authPlugin);
87pluginManager.register(loggingPlugin);
88
89// Request simulation
90const context = {
91 user: { id: "123", name: "Jan" },
92 request: { url: "/api/data", method: "GET" }
93};
94
95pluginManager.execute("beforeRequest", context);1// Extending Material-UI
2declare module "@mui/material/styles" {
3 interface Theme {
4 custom: {
5 borderRadius: {
6 small: number;
7 medium: number;
8 large: number;
9 };
10 animations: {
11 fast: string;
12 normal: string;
13 slow: string;
14 };
15 };
16 }
17
18 interface ThemeOptions {
19 custom?: {
20 borderRadius?: {
21 small?: number;
22 medium?: number;
23 large?: number;
24 };
25 animations?: {
26 fast?: string;
27 normal?: string;
28 slow?: string;
29 };
30 };
31 }
32
33 interface TypographyVariants {
34 label: React.CSSProperties;
35 caption2: React.CSSProperties;
36 }
37
38 interface TypographyVariantsOptions {
39 label?: React.CSSProperties;
40 caption2?: React.CSSProperties;
41 }
42}
43
44declare module "@mui/material/Typography" {
45 interface TypographyPropsVariantOverrides {
46 label: true;
47 caption2: true;
48 }
49}
50
51// Creating an extended theme
52import { createTheme } from "@mui/material/styles";
53
54const theme = createTheme({
55 custom: {
56 borderRadius: {
57 small: 4,
58 medium: 8,
59 large: 16
60 },
61 animations: {
62 fast: "150ms",
63 normal: "300ms",
64 slow: "500ms"
65 }
66 },
67 typography: {
68 label: {
69 fontSize: "0.75rem",
70 fontWeight: 600,
71 textTransform: "uppercase"
72 },
73 caption2: {
74 fontSize: "0.625rem",
75 fontWeight: 400
76 }
77 }
78});
79
80// Using extended types
81import { Typography, Box } from "@mui/material";
82
83function CustomComponent() {
84 return (
85 <Box
86 sx={{
87 borderRadius: (theme) => theme.custom.borderRadius.medium,
88 transition: (theme) => `all ${theme.custom.animations.normal} ease`
89 }}
90 >
91 <Typography variant="label">Custom Label</Typography>
92 <Typography variant="caption2">Tiny caption text</Typography>
93 </Box>
94 );
95}1// Extending Next.js
2declare module "next" {
3 export interface NextApiRequest {
4 session?: {
5 user: {
6 id: string;
7 email: string;
8 permissions: string[];
9 };
10 expiresAt: Date;
11 };
12
13 // Helper methods
14 validateBody<T>(schema: any): T;
15 requireAuth(): void;
16 hasPermission(permission: string): boolean;
17 }
18
19 export interface NextApiResponse {
20 success<T>(data: T, statusCode?: number): void;
21 error(message: string, statusCode?: number): void;
22 paginated<T>(items: T[], total: number, page: number, pageSize: number): void;
23 }
24}
25
26// Middleware extending Next.js
27import { NextApiRequest, NextApiResponse } from "next";
28
29export function withExtensions(
30 handler: (req: NextApiRequest, res: NextApiResponse) => Promise<void>
31) {
32 return async (req: NextApiRequest, res: NextApiResponse) => {
33 // Add methods to request
34 req.validateBody = function<T>(schema: any): T {
35 // Validation implementation
36 return req.body as T;
37 };
38
39 req.requireAuth = function() {
40 if (!this.session) {
41 throw new Error("Authentication required");
42 }
43 };
44
45 req.hasPermission = function(permission: string): boolean {
46 return this.session?.user.permissions.includes(permission) || false;
47 };
48
49 // Add methods to response
50 res.success = function<T>(data: T, statusCode = 200) {
51 this.status(statusCode).json({
52 success: true,
53 data
54 });
55 };
56
57 res.error = function(message: string, statusCode = 500) {
58 this.status(statusCode).json({
59 success: false,
60 error: message
61 });
62 };
63
64 res.paginated = function<T>(items: T[], total: number, page: number, pageSize: number) {
65 this.json({
66 success: true,
67 data: items,
68 pagination: {
69 total,
70 page,
71 pageSize,
72 totalPages: Math.ceil(total / pageSize)
73 }
74 });
75 };
76
77 // Call handler
78 try {
79 await handler(req, res);
80 } catch (error) {
81 res.error(error instanceof Error ? error.message : "Internal server error");
82 }
83 };
84}
85
86// Usage in API route
87export default withExtensions(async (req, res) => {
88 req.requireAuth();
89
90 if (!req.hasPermission("read:users")) {
91 return res.error("Insufficient permissions", 403);
92 }
93
94 const data = req.validateBody<{ search?: string }>(/* schema */);
95
96 // Business logic
97 const users = []; // Fetch users
98 const total = 100;
99
100 res.paginated(users, total, 1, 20);
101});Module augmentation and declaration merging are powerful tools that enable flexible extension of existing code. The key is understanding when to use each technique and how they can work together in creating extensible systems.