It's the sanctioned way to extend library types, like adding a user property to an Express request or a field to Window.
TypeScript lets several declarations with the same name combine into one entity — declaration merging.
Interfaces merge: declaring interface Box { width: number } twice with different members gives one interface with all of them. Non-function members must not conflict; methods with the same name become overloads.
Namespaces merge with functions, classes and enums, which lets you attach static-like members to a function or class — the pattern behind many older JavaScript libraries' typings (function $() that also has $.ajax).
The practical payoff is module augmentation: adding to types declared by another module. In a file that is itself a module (it has an import or export), write:
import "express";
declare module "express-serve-static-core" {
interface Request { user?: { id: string } }
}You augment the module that actually declares the interface (here Express's internal types package), and you can only add to existing declarations — not add new top-level exports.
Global augmentation works the same way for global types, using declare global { ... } inside a module:
declare global {
interface Window { analytics: Analytics }
}
export {};The export {} matters: in a file without imports or exports, declare global is not allowed, and a plain interface Window in such a script file merges globally anyway — confusing.
Augmentations live in .ts or .d.ts files that the project includes. Keep them in one obvious place (e.g. src/types/), because they change types everywhere and are easy to forget. Augmenting built-ins like Array.prototype is possible but pollutes every consumer of your code; libraries should avoid it.
Example
// Interface merging
interface Box { width: number }
interface Box { height: number }
const box: Box = { width: 2, height: 3 };
// Function + namespace merging: a callable with properties
function format(value: number): string {
return value.toFixed(format.precision);
}
namespace format {
export let precision = 2;
}
format.precision = 1;
// Global augmentation (this file is a module because of the export below)
declare global {
interface Array<T> {
last(): T | undefined;
}
}
Object.defineProperty(Array.prototype, "last", {
value: function <T>(this: T[]) { return this[this.length - 1]; },
});
console.log(box.width * box.height, format(3.14159), [1, 2, 3].last());
export {};Remember
- Same-named interfaces merge; methods become overloads
- Namespaces can merge into functions, classes and enums
declare module "pkg"augments a package;declare globalaugments globals- Augmentations must be in a module file and included by the project
Watch out for
- Augmenting the re-exporting module instead of the module that declares the interface, so nothing merges.