Declaration Merging Lets Types Accumulate Across Files
How interfaces, namespaces, and module augmentation extend declarations without changing runtime objects.
Some TypeScript declarations with the same name combine into one symbol. This supports JavaScript patterns where functions carry properties, namespaces span files, or consumers add types for plugin methods.
Interfaces are the everyday example:
interface RequestContext {
requestId: string;
}
interface RequestContext {
user?: { id: string };
}
const context: RequestContext = {
requestId: 'r-1',
user: { id: 'u-2' },
};
The compiler treats RequestContext as having both properties. Non-function members with the same name must have compatible types. Function members form an overload set, with later groups generally ordered before earlier groups.
Type aliases participate too
Type aliases merge under the same rules as interfaces. Splitting a large object type across files can therefore be done with repeated type Configuration = ... declarations, and libraries may augment a public type alias without changing its source.
The distinction between aliases and interfaces is mostly syntactic for object shapes. Both can be reopened, while aliases additionally represent unions, primitives, tuples, and mapped types.
Namespaces can add a static side
A namespace can merge with a function, class, or enum to describe properties attached at runtime.
function build(input: string) {
return input.toUpperCase();
}
namespace build {
export const version = '2.0';
}
build('draft');
console.log(build.version);
The emitted namespace code actually assigns version to the function object. This is different from interface merging, which is erased. Only exported namespace members contribute to the merged public shape.
Module augmentation patches an imported module
An external module can be augmented by declaring it with the same module specifier used by consumers.
import { Observable } from './observable.js';
declare module './observable.js' {
interface Observable<T> {
map<U>(fn: (value: T) => U): Observable<U>;
}
}
Observable.prototype.map = function (fn) {
// runtime implementation
};
The declaration changes only the compiler’s view. The prototype assignment—or another runtime import with the side effect—is still required. Forgetting that half creates code that checks successfully and fails when called.
An augmentation may also introduce entirely new top-level exports from the target module. Declaring a new function inside declare module makes it importable even if the original module had no declaration by that name; consumers need only ensure a plugin supplies it at runtime.
Augmentations cannot target a module’s default export by a local alias because augmentation names refer to exported declarations. Named exports provide more reliable extension points.
Global augmentation is broader
Inside an external module, declare global can extend global interfaces such as Window. This is appropriate for a real runtime installation performed by a script, but it affects every file in the compilation. Prefer a module-scoped API when global ownership is not intrinsic.
Declaration merging is powerful because it models open JavaScript systems. It is also a promise about runtime reality. Keep the type augmentation adjacent to the code that installs the behaviour, and choose interfaces as public extension points only when openness is intentional.