Declaration Merging
Declaration merging is the compiler’s ability to combine two or more separate declarations that share the same name into a single definition. The most common form is interface merging — declaring an interface twice adds the members together — but the mechanism also covers namespaces, and merging across declarations of different kinds (interface + namespace, function + namespace). It is the foundation of module augmentation, the technique libraries and apps use to extend types they do not own. This page covers interface merging, namespace merging, and the basics of augmenting external and global types.
Merging interfaces
When the same interface name appears more than once in the same scope, TypeScript merges all the members into one interface. Non-function members must not conflict; function members of the same name become overloads.
interface Box {
height: number;
width: number;
}
interface Box {
scale: number;
}
const box: Box = { height: 5, width: 6, scale: 10 }; // ✅ has all three
The two Box declarations behave as if you had written a single interface with all three members. This is the property that distinguishes interfaces from type aliases, which cannot be redeclared.
Merging order and function overloads
When merged interfaces declare the same method name, the signatures combine into an overload set. Later declarations in the same file are ordered before earlier ones, except that signatures with single literal-string parameters are prioritized to the top.
interface Document {
createElement(tag: "div"): HTMLDivElement;
}
interface Document {
createElement(tag: "span"): HTMLSpanElement;
createElement(tag: string): HTMLElement;
}
// merged overloads: "div", "span", then the general string fallback
This is exactly how the DOM lib types build up Document.createElement across multiple lib.dom.d.ts declarations, giving precise return types for known tag names.
Namespace merging
Namespaces merge with each other and, importantly, with classes, functions, and enums of the same name. Merging a namespace into a function or class is how you attach static-like members or nested types to a value.
function greet(name: string): string {
return `${greet.prefix} ${name}`;
}
namespace greet {
export let prefix = "Hello";
}
greet("World"); // "Hello World"
Here the greet function and the greet namespace merge, so greet.prefix is a valid, typed property on the function. Only exported members of the namespace become visible on the merged result.
Module augmentation
To add members to a type declared in another module, reopen it inside a declare module block. This is module augmentation — the most practical use of declaration merging in application code. A common example is adding a property to Express’s Request.
import "express";
declare module "express-serve-static-core" {
interface Request {
user?: { id: string; roles: string[] };
}
}
After this augmentation, req.user is typed everywhere in the project. The file must be a module (it has an import/export) and must be included by your tsconfig so the compiler picks it up.
The augmenting file must itself be a module. If it has no top-level
importorexport,declare module "x"is interpreted as an ambient module declaration that overwrites the original instead of merging into it.
Augmenting the global scope
To extend global types — like adding a property to window or globalThis, or a variable to NodeJS.ProcessEnv — use declare global from inside a module file.
export {}; // ensures this file is a module
declare global {
interface Window {
dataLayer: unknown[];
}
namespace NodeJS {
interface ProcessEnv {
DATABASE_URL: string;
NODE_ENV: "development" | "production" | "test";
}
}
}
The export {} turns the file into a module so declare global is valid, and the merged Window/ProcessEnv interfaces gain the new members project-wide.
What can and cannot merge
| Declaration kinds | Merge? |
|---|---|
| interface + interface | Yes (members combine) |
| namespace + namespace | Yes |
| namespace + class / function / enum | Yes |
| type alias + anything | No (duplicate identifier error) |
| interface with conflicting non-function member | No (error) |
| class + class | No |
Best Practices
- Reach for interface merging only intentionally; accidental duplicate names cause confusion.
- Use module augmentation to type third-party libraries instead of casting to
any. - Ensure augmenting files are modules (have an
import/export) so merging works. - Use
declare globalwithexport {}to extendwindow,globalThis, orProcessEnv. - Keep augmentations in dedicated
*.d.tsfiles and include them intsconfig. - Remember type aliases cannot merge — use interfaces when you need extensibility.
- Prefer narrow, well-documented augmentations over broad global changes.