TypeScript 58 🔷 Namespaces and Declaration Merging
TypeScript has two features that predate the module system and remain in the language for compatibility and for a specific set of use cases: namespaces and declaration merging. A namespace is a way to group related types, values, and functions under a single name, using the namespace keyword. Declaration merging is the ability to declare the same name more than once, with the declarations combining into a single entity — an interface declared twice has the members of both, and a namespace merged with a function adds properties to the function. These features are not the modern way to organize code — ES modules are — but they appear in real codebases, in type declarations for libraries, and in the augmentation of existing types. Knowing when they are the right tool and when they are a legacy pattern is part of reading TypeScript fluently. This chapter covers namespaces, the four kinds of declaration merging, module augmentation, global augmentation, and the rules that determine when a merge succeeds and when it is a conflict.
Key point: A namespace groups declarations under a name and can contain types, values, and nested namespaces. Declaration merging is the rule that two declarations of the same name combine: two interfaces merge their members, a namespace merged with a function or class adds properties to it, and two namespaces merge their exports. Modules do not merge — a file that has a top-level import or export is a module, and its declarations are scoped to the file. Module augmentation is the mechanism for adding declarations to an existing module from outside it, and global augmentation is the mechanism for adding to the global scope. The .d.ts files that libraries ship use these features heavily, and reading them requires understanding the merge rules.
What namespaces are
A namespace is a container for related declarations. It uses the namespace keyword, and its members are accessed with the dot notation.
namespace Validation {
export interface StringValidator {
isValid(s: string): boolean;
}
export const lettersRegexp = /^[A-Za-z]+$/;
export class LettersOnlyValidator implements StringValidator {
isValid(s: string): boolean {
return lettersRegexp.test(s);
}
}
}
const validator = new Validation.LettersOnlyValidator();
console.log(validator.isValid("Hello"));
The Validation namespace contains an interface, a constant, and a class. The export keyword inside the namespace makes the members accessible from outside. The members are accessed with Validation.LettersOnlyValidator.
Why the export inside a namespace is required. A namespace’s members are private by default. Only the members marked export are visible outside. This is the opposite of the module behavior, where everything at the top level is exported by default. The distinction is historical and a source of confusion.
Why namespaces exist. Before ES modules, JavaScript had no module system in the language. Namespaces were TypeScript’s answer: they wrapped related declarations in a container and prevented name collisions in the global scope. The pattern was the standard way to organize a library.
Why namespaces are not the modern recommendation. ES modules replaced the need for namespaces. A module is a file, and the file’s exports are the public interface. The module system is part of the language, works in every runtime, and is the standard. Namespaces are a legacy feature, and new code should use modules.
Why namespaces still appear. The declare global blocks, the type declarations for libraries that predate modules, and the ambient declarations in .d.ts files use namespaces. The NodeJS, Express, and JQuery global namespaces in the DefinitelyTyped declarations are examples. Reading them requires understanding the namespace syntax.
Why a namespace can be nested. A namespace can contain another namespace, which produces a deeper hierarchy. The nesting is a way to organize a large set of declarations, and the dot notation extends: A.B.C.
namespace A {
export namespace B {
export const c = 1;
}
}
console.log(A.B.c);
The nesting is a convention, and the modern equivalent is a directory structure with modules.
Why the namespace keyword replaced module. The module keyword was used for internal modules, and the ES module syntax was added later. To avoid confusion, the module keyword was renamed to namespace. The old module Foo {} form is still accepted but deprecated, and the namespace Foo {} form is preferred.
Why a namespace can be used in a .d.ts file. An ambient namespace declares the shape of a global namespace that exists at runtime. The declare namespace form is used when the runtime provides the namespace and TypeScript needs to know its shape. This is common in the type declarations for libraries that attach themselves to the global.
The four kinds of declaration merging
Declaration merging is the rule that two declarations of the same name combine. TypeScript supports four kinds, each with its own rules about what can merge with what.
Interface with interface. Two interfaces with the same name in the same scope merge their members. The result has all the members of both.
interface User {
id: string;
}
interface User {
name: string;
}
const user: User = { id: "1", name: "Alice" };
The two declarations produce a single User interface with both id and name. The order does not matter, and the merge is the standard behavior.
Why the interface merge is the most common. The interface merge is the one that appears most often. It is how a library’s types are extended, how a module’s types are augmented, and how the declare global blocks add to the global scope. The rule is simple and predictable.
Namespace with namespace. Two namespaces with the same name merge their exports. The result has all the exported members of both.
namespace Utils {
export const a = 1;
}
namespace Utils {
export const b = 2;
}
console.log(Utils.a, Utils.b);
The two namespaces merge, and Utils has both a and b. The merge is what makes the namespace extensible across files.
Namespace with function, class, or enum. A namespace can merge with a function, a class, or an enum, and the namespace’s exports become properties of the function or class.
function greet(name: string): string {
return `Hello, ${name}`;
}
namespace greet {
export const defaultName = "world";
export function loudly(name: string): string {
return greet(name).toUpperCase();
}
}
console.log(greet("Alice"));
console.log(greet.defaultName);
console.log(greet.loudly("Bob"));
The function greet has the properties defaultName and loudly added by the namespace. The function and the namespace merge, and the namespace’s exports become properties of the function.
Why the function-namespace merge exists. The pattern is the way to attach helper properties to a function. A library that exports a function and wants to attach a configuration or a set of helpers uses the pattern. The moment.js library’s moment.duration, moment.locale, and similar properties are the result of this merge.
Why the merge requires the function to be declared first. The function (or class, or enum) must be declared before the namespace that merges with it. The reverse order is an error because the namespace’s exports are added to the function, and the function must exist for the properties to be assigned.
Class with namespace. A class and a namespace with the same name merge, and the namespace’s exports become static properties of the class.
class Point {
constructor(public x: number, public y: number) {}
}
namespace Point {
export const origin = new Point(0, 0);
}
console.log(Point.origin);
The class Point has the static property origin from the namespace. This is the way to add static members to a class that were not part of the class body.
Enum with namespace. An enum and a namespace with the same name merge, and the namespace’s exports become members of the enum. The pattern is used to add helper functions to an enum.
Why the four kinds are the complete set. The rules are: interfaces merge with interfaces, namespaces merge with namespaces, and namespaces merge with functions, classes, or enums. Nothing else merges. Two classes with the same name do not merge, two variables with the same name do not merge (they conflict), and a class and an interface with the same name do not merge.
Why the merge is the same-name-in-the-same-scope rule. The merge happens when the two declarations have the same name and are in the same scope. Two declarations in different scopes do not merge. Two declarations in different modules do not merge unless the module augmentation is used.
Modules do not merge
A file that has a top-level import or export is a module. Its declarations are scoped to the file, and they do not merge with the declarations in another module.
// a.ts
export interface User { id: string; }
// b.ts
export interface User { name: string; }
// The two User interfaces are different types.
// They do not merge.
The two User interfaces are in different modules, and each is scoped to its file. The merge rule does not apply. To combine them, one module must import the other and use the imported type, or the module augmentation must be used.
Why modules do not merge. A module’s exports are its public interface, and the module system is designed to keep the interfaces separate. Allowing two modules to merge would reintroduce the global-scope problems that modules were designed to solve.
Why the distinction is important. A developer who expects the interface merge to work across modules is surprised when it does not. The merge is a same-scope rule, and modules are separate scopes. The cross-module combination is the module augmentation, which is a different mechanism.
Why the declare global block is the exception. A declare global block inside a module adds declarations to the global scope, which is the one place where a module can affect another module’s scope. The block is the mechanism for adding to the global, and it is used in the type declarations for libraries that extend the global.
Why the module scope is the modern default. The module scope is what makes the code predictable. Each file has its own namespace, and the exports are the interface. The global scope is reserved for the environment — window, document, console — and the module scope is for the application.
Module augmentation
Module augmentation is the mechanism for adding declarations to an existing module from outside it. It is the way to extend a library’s types, add a method to a third-party class, or add a property to a framework’s interface.
// express.d.ts
import 'express';
declare module 'express' {
interface Request {
user?: { id: string; name: string };
}
}
The declare module 'express' block adds a user property to the Request interface. The interface merge applies, and the augmented Request has the new property in the rest of the code.
Why the import 'express' is required. The import makes the file a module and brings the express types into scope. Without it, the declare module 'express' would be a declaration of a new module, not an augmentation of the existing one. The import is the marker that the file is augmenting, not declaring.
Why the augmentation is a common pattern. A library’s types are often insufficient for the application’s needs. The augmentation adds the missing properties without modifying the library. The express example is the canonical one: the middleware adds a user property to the request, and the augmentation declares it.
Why the augmentation must match the module’s export shape. The declare module block must use the same name as the module’s import specifier. The express module is imported as 'express', and the augmentation uses 'express'. A mismatch means the augmentation does not apply.
Why the augmentation is applied globally. Once the augmentation file is included in the compilation, the augmented types are used everywhere. The file is usually a .d.ts file in the project, and the tsconfig.json‘s include or files ensures it is compiled. The augmentation is not imported; it is applied by being part of the compilation.
Why the augmentation can add new exports. The declare module block can add new exports to the module, not just augment the existing ones.
declare module 'my-library' {
export function newFunction(): void;
}
The new export is available to any code that imports from my-library. This is the way to add a function to a library’s public interface.
Why the augmentation can be used for a global library. A library that attaches itself to the global — jQuery, lodash as _, moment — is augmented through the global scope. The declare global block is the mechanism.
Global augmentation
Global augmentation is the mechanism for adding declarations to the global scope from inside a module. It uses the declare global block.
// global.d.ts
export {};
declare global {
interface Window {
myApp: {
version: string;
config: Record<string, unknown>;
};
}
var myGlobal: string;
}
The declare global block adds a myApp property to the Window interface and a myGlobal variable to the global scope. The additions are visible to every file in the compilation.
Why the export {} is required. The export {} makes the file a module, which is a prerequisite for the declare global block. Without it, the file is a script, and the declare global block is not allowed. The export {} is the standard way to make a file a module without exporting anything meaningful.
Why the global augmentation is used for browser APIs. A library that adds a property to window — a version string, a configuration object, an analytics hook — declares the property in a declare global block. The augmentation makes the property visible to the TypeScript code that uses it.
Why the augmentation is scoped to the compilation. The declare global block’s additions are visible in the files included in the compilation. A project that includes the file gets the additions; a project that does not, does not. The file is usually in the project’s src directory and included by the tsconfig.json.
Why the global augmentation should be used sparingly. Adding to the global scope is a claim on a shared namespace. A property named myApp could conflict with another library’s property. The augmentation should use a distinctive name, and the global scope should be extended only when the runtime actually provides the global.
Why the interface Window merge is the common case. The Window interface is declared by the DOM types, and the augmentation merges a new property into it. The merge is the interface merge, applied to a global interface. The pattern is the same as any other interface merge.
Why the global augmentation can add to the NodeJS namespace. The NodeJS namespace contains the Node.js global types, and the augmentation adds to it.
declare global {
namespace NodeJS {
interface ProcessEnv {
MY_VAR: string;
}
}
}
The augmentation adds a MY_VAR property to ProcessEnv, which is the type of process.env. The environment variable is typed, and the code that reads it does not need a cast.
Why the augmentation is the standard pattern for environment variables. The process.env object is typed as Record<string, string | undefined> by default. The augmentation declares the specific variables, which makes the code that reads them type-safe. The pattern is used in every Node.js project that has environment configuration.
Why the augmentation is a double-edged feature. It makes a library’s types extensible, which is essential for the middleware pattern. It also allows a project to modify another library’s types in ways that can surprise. The rule is to augment only when the runtime actually provides the addition — a property that exists but is not declared, or a variable that the environment sets. An augmentation that declares something the runtime does not provide is a lie to the compiler.
Rules and conflicts
Declaration merging follows rules, and a violation is a compile error. Knowing the rules is what makes the merges predictable.
The same-name rule. The declarations must have the same name. Two different names do not merge; they are separate declarations.
The same-scope rule. The declarations must be in the same scope. Two interfaces in the same module merge; two in different modules do not. The global scope and the module scope are different scopes.
The compatible-kind rule. The declarations must be of compatible kinds. Two interfaces merge; two classes do not; an interface and a class do not. The four kinds are the compatible combinations.
The no-conflict rule. The merged declarations must not have conflicting members. Two interfaces with the same property name and different types is an error.
interface User {
id: string;
}
interface User {
id: number; // ❌ error: subsequent property declarations must have the same type
}
The id property is declared twice with different types, and the merge is a conflict. The error is the compiler’s report of the incompatibility.
Why the conflict rule is important. The merge is a convenience, not a way to override. A conflicting member is a mistake, and the compiler reports it. The fix is to remove one of the declarations or to make the types agree.
Why the function-namespace merge must be ordered. The function (or class, or enum) must be declared before the namespace. The reverse order is an error because the namespace’s exports are added to the function, and the function must exist for the addition.
Why the augmentation must be in a module. The declare global block requires the file to be a module, which is why the export {} is needed. The declare module block works in a module or a script, but the augmentation of an existing module requires the file to be a module to use the import.
Why the merge is not a runtime feature. The merge is a compile-time operation. The declarations do not exist at runtime — the types are erased, and the namespaces that contain values become objects or IIFEs. The merge is TypeScript’s way of combining declarations that the runtime already provides separately.
Why the merge rules are worth knowing. The merge appears in the .d.ts files of every library, in the declare global blocks of every project that extends the global, and in the module augmentations of every application that extends a library. Reading these files requires the rules, and writing them requires understanding what can merge with what.
Complete Example Session
// ============================================
// PART 1: INTERFACE MERGE
// ============================================
interface User {
id: string;
}
interface User {
name: string;
}
const user: User = { id: "1", name: "Alice" };
// User has both id and name
// ============================================
// PART 2: NAMESPACE
// ============================================
namespace Validation {
export interface StringValidator {
isValid(s: string): boolean;
}
export const lettersRegexp = /^[A-Za-z]+$/;
export class LettersOnlyValidator implements StringValidator {
isValid(s: string): boolean {
return lettersRegexp.test(s);
}
}
}
const validator = new Validation.LettersOnlyValidator();
console.log(validator.isValid("Hello"));
// ============================================
// PART 3: NAMESPACE MERGE
// ============================================
namespace Utils {
export const a = 1;
}
namespace Utils {
export const b = 2;
}
console.log(Utils.a, Utils.b);
// ============================================
// PART 4: FUNCTION AND NAMESPACE MERGE
// ============================================
function greet(name: string): string {
return `Hello, ${name}`;
}
namespace greet {
export const defaultName = "world";
export function loudly(name: string): string {
return greet(name).toUpperCase();
}
}
console.log(greet("Alice"));
console.log(greet.defaultName);
console.log(greet.loudly("Bob"));
// ============================================
// PART 5: CLASS AND NAMESPACE MERGE
// ============================================
class Point {
constructor(public x: number, public y: number) {}
}
namespace Point {
export const origin = new Point(0, 0);
}
console.log(Point.origin);
// ============================================
// PART 6: MODULE AUGMENTATION
// ============================================
// express.d.ts
import 'express';
declare module 'express' {
interface Request {
user?: { id: string; name: string };
}
}
// The Request interface now has a user property.
// ============================================
// PART 7: GLOBAL AUGMENTATION
// ============================================
// global.d.ts
export {};
declare global {
interface Window {
myApp: {
version: string;
};
}
var myGlobal: string;
}
// window.myApp is now typed.
// ============================================
// PART 8: ENVIRONMENT VARIABLES
// ============================================
// env.d.ts
declare global {
namespace NodeJS {
interface ProcessEnv {
DATABASE_URL: string;
PORT: string;
}
}
}
export {};
// process.env.DATABASE_URL is now typed.
// ============================================
// PART 9: CONFLICT
// ============================================
// interface User {
// id: string;
// }
//
// interface User {
// id: number; // ❌ subsequent property declarations must have the same type
// }
// ============================================
// PART 10: WHAT NOT TO DO
// ============================================
// Don't use namespaces for new code
// Use ES modules.
// Don't expect modules to merge
// Each module has its own scope.
// Don't augment without the runtime providing it
// The augmentation is a claim.
// Don't declare the namespace before the function
// The function must come first.
// Don't conflict on members
// The merged declarations must agree.
// Don't use a broad global name
// Use a distinctive one to avoid collisions.
The ten parts cover interface merge, namespaces, namespace merge, function-namespace merge, class-namespace merge, module augmentation, global augmentation, environment variables, conflicts, and the anti-patterns.
Quick Reference
Declaration Merging Kinds
| First | Second | Result |
|---|---|---|
| Interface | Interface | Merged members |
| Namespace | Namespace | Merged exports |
| Function | Namespace | Namespace exports become properties |
| Class | Namespace | Namespace exports become static members |
| Enum | Namespace | Namespace exports become members |
What Does Not Merge
| First | Second | Result |
|---|---|---|
| Class | Class | Conflict |
| Variable | Variable | Conflict |
| Module | Module | Separate scopes |
| Type alias | Type alias | Conflict |
Namespace Syntax
| Form | Purpose |
|---|---|
namespace Foo {} | Internal namespace |
declare namespace Foo {} | Ambient namespace |
export inside | Make a member public |
Augmentation
| Form | Purpose |
|---|---|
declare module 'name' {} | Augment a module |
declare global {} | Augment the global scope |
export {} | Make the file a module |
The Merge Rules
| Rule | Description |
|---|---|
| Same name | The declarations must have the same name |
| Same scope | Same file or same module |
| Compatible kind | One of the four combinations |
| No conflict | Members must not conflict |
Common Augmentation Targets
| Target | Purpose |
|---|---|
express.Request | Add middleware properties |
Window | Add browser globals |
NodeJS.ProcessEnv | Type environment variables |
JQueryStatic | Add jQuery plugins |
Best Practices
✅ Do This:
// Use interfaces and let them merge
interface User { id: string; }
interface User { name: string; } // ✅
// Use module augmentation to extend a library
declare module 'express' {
interface Request { user?: User; }
} // ✅
// Use global augmentation for environment variables
declare global {
namespace NodeJS { interface ProcessEnv { PORT: string; } }
}
export {}; // ✅
// Use the `export {}` to make a file a module
export {}; // ✅
// Declare the function before the namespace
function greet() {}
namespace greet { export const x = 1; } // ✅
// Use ES modules for new code
export function format() {} // ✅
❌ Don’t Do This:
// Don't use namespaces for new code
namespace Utils { export function format() {} } // ⚠️
// Don't expect modules to merge
// export interface User {} in two modules are different // ⚠️
// Don't declare the namespace before the function
namespace greet { export const x = 1; }
function greet() {} // ❌ // ⚠️
// Don't conflict on members
interface User { id: string; }
interface User { id: number; } // ❌ // ⚠️
// Don't augment without the runtime providing it
declare module 'x' { interface Y { z: number; } } // claim // ⚠️
// Don't use a broad global name
declare global { var app: any; } // collision risk // ⚠️
// Don't use `module` instead of `namespace`
module Foo {} // deprecated // ⚠️
Common Pitfalls
| Pitfall | Problem | Solution |
|---|---|---|
| Modules expected to merge | Separate scopes | Use augmentation |
| Namespace before function | Merge error | Function first |
| Conflicting members | Compile error | Make the types agree |
declare global in a script | Not allowed | Add export {} |
| Missing import in augmentation | Declares a new module | Import the module |
| Broad global name | Collision | Use a distinctive name |
module instead of namespace | Deprecated | Use namespace |
| Augmentation without runtime | Lie to the compiler | Augment only what exists |
Real-World Examples
1. Interface merge
interface User { id: string; }
interface User { name: string; }
2. Namespace
namespace Validation {
export interface Validator {}
}
3. Namespace merge
namespace Utils { export const a = 1; }
namespace Utils { export const b = 2; }
4. Function-namespace merge
function greet() {}
namespace greet { export const x = 1; }
5. Class-namespace merge
class Point {}
namespace Point { export const origin = new Point(); }
6. Express augmentation
declare module 'express' {
interface Request { user?: User; }
}
7. Global Window
declare global {
interface Window { myApp: App; }
}
8. Environment variables
declare global {
namespace NodeJS { interface ProcessEnv { PORT: string; } }
}
9. jQuery plugin
declare global {
interface JQuery { myPlugin(): JQuery; }
}
10. Module augmentation with new export
declare module 'my-lib' {
export function newFn(): void;
}
Visual: Declaration Merging
┌──────────────────────────────────────────────────────────┐
│ INTERFACE + INTERFACE │
│ │
│ interface User { id: string; } │
│ interface User { name: string; } │
│ │ │
│ ▼ │
│ interface User { id: string; name: string; } │
│ │
├──────────────────────────────────────────────────────────┤
│ FUNCTION + NAMESPACE │
│ │
│ function greet() {} │
│ namespace greet { export const x = 1; } │
│ │ │
│ ▼ │
│ function greet() {} │
│ greet.x = 1 │
│ │
├──────────────────────────────────────────────────────────┤
│ CLASS + NAMESPACE │
│ │
│ class Point {} │
│ namespace Point { export const origin = ...; } │
│ │ │
│ ▼ │
│ class Point {} │
│ Point.origin = ... │
│ │
└──────────────────────────────────────────────────────────┘
Visual: Modules Do Not Merge
┌──────────────────────────────────────────────────────────┐
│ a.ts │
│ export interface User { id: string; } │
│ │
│ b.ts │
│ export interface User { name: string; } │
│ │
│ The two User interfaces are DIFFERENT types. │
│ Each is scoped to its module. │
│ They do not merge. │
│ │
├──────────────────────────────────────────────────────────┤
│ AUGMENTATION │
│ │
│ // augment.ts │
│ import 'express'; │
│ declare module 'express' { │
│ interface Request { user?: User; } │
│ } │
│ │
│ The Request interface is augmented. │
│ The augmentation is the cross-module mechanism. │
│ │
└──────────────────────────────────────────────────────────┘
Visual: Module Augmentation
┌──────────────────────────────────────────────────────────┐
│ node_modules/express/index.d.ts │
│ export interface Request { ... } │
│ │
│ src/express.d.ts │
│ import 'express'; │
│ declare module 'express' { │
│ interface Request { user?: User; } │
│ } │
│ │ │
│ ▼ │
│ The interface merge applies. │
│ express.Request now has a user property. │
│ │
│ The runtime provides the property (middleware sets it). │
│ The augmentation declares it. │
│ │
└──────────────────────────────────────────────────────────┘
Visual: Global Augmentation
┌──────────────────────────────────────────────────────────┐
│ src/global.d.ts │
│ export {}; │
│ │ │
│ └── makes the file a module │
│ │
│ declare global { │
│ interface Window { myApp: App; } │
│ var myGlobal: string; │
│ } │
│ │ │
│ ▼ │
│ window.myApp is typed everywhere in the project. │
│ │
│ The runtime provides window.myApp. │
│ The augmentation declares it. │
│ │
└──────────────────────────────────────────────────────────┘
Visual: The Merge Rules
┌──────────────────────────────────────────────────────────┐
│ Same name? │
│ ├── No ──► separate declarations │
│ └── Yes │
│ │ │
│ ▼ │
│ Same scope? │
│ ├── No ──► separate declarations │
│ └── Yes │
│ │ │
│ ▼ │
│ Compatible kind? │
│ ├── No ──► conflict │
│ └── Yes │
│ │ │
│ ▼ │
│ Conflicting members? │
│ ├── Yes ──► compile error │
│ └── No ──► merge │
│ │
└──────────────────────────────────────────────────────────┘
Visual: Namespaces vs Modules
┌──────────────────────────────────────────────────────────┐
│ NAMESPACE (legacy) │
│ │
│ namespace Utils { │
│ export function format() {} │
│ } │
│ │
│ Utils.format() │
│ │
│ Global container. Not a file. │
│ Used for pre-module libraries. │
│ │
├──────────────────────────────────────────────────────────┤
│ MODULE (modern) │
│ │
│ // utils.ts │
│ export function format() {} │
│ │
│ // app.ts │
│ import { format } from './utils'; │
│ format() │
│ │
│ File-scoped. The standard. │
│ │
└──────────────────────────────────────────────────────────┘
Summary
| Feature | Purpose |
|---|---|
| Namespace | Group declarations under a name |
| Declaration merging | Combine same-name declarations |
| Module augmentation | Add to an existing module |
| Global augmentation | Add to the global scope |
| Merge | Result |
|---|---|
| Interface + Interface | Merged members |
| Namespace + Namespace | Merged exports |
| Function + Namespace | Namespace exports as properties |
| Class + Namespace | Namespace exports as statics |
| Enum + Namespace | Namespace exports as members |
| Augmentation | Syntax |
|---|---|
| Module | declare module 'name' {} |
| Global | declare global {} + export {} |
| Environment | declare global { namespace NodeJS { interface ProcessEnv {} } } |
Key takeaways:
- A namespace groups related declarations under a name — the members are private by default and must be marked
exportto be visible outside - Namespaces are a legacy feature — ES modules replaced them, and new code should use modules
- Declaration merging combines same-name declarations — interfaces merge with interfaces, namespaces merge with namespaces, and namespaces merge with functions, classes, and enums
- Modules do not merge — a file with a top-level import or export has its own scope, and its declarations do not combine with another module’s
- Module augmentation adds to an existing module — the
declare module 'name'block extends the module’s types, and theimport 'name'is required to make the file a module - Global augmentation adds to the global scope — the
declare globalblock requires the file to be a module, which is whyexport {}is needed - The environment variable pattern uses global augmentation — the
NodeJS.ProcessEnvinterface is augmented to type the variables - The merge rules are same name, same scope, compatible kind, no conflict — a violation is a compile error
- The augmentation is a claim about the runtime — it should declare only what the runtime provides, or it lies to the compiler
- The
.d.tsfiles of every library use these features — reading them requires understanding the merge rules and the augmentation syntax
Remember: Namespaces and declaration merging are the pre-module mechanisms that remain in TypeScript for compatibility and for a specific set of use cases. The interface merge and the module augmentation are the ones that appear in real codebases, and the global augmentation is the standard for environment variables and browser globals. Use ES modules for new code, and use the augmentation when a library’s types need to be extended. The rules are simple — same name, same scope, compatible kind, no conflict — and the merge is the convenience that makes the type declarations of the ecosystem work.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!