Skip to content
TypeScript ts interfaces 4 min read

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 import or export, 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 kindsMerge?
interface + interfaceYes (members combine)
namespace + namespaceYes
namespace + class / function / enumYes
type alias + anythingNo (duplicate identifier error)
interface with conflicting non-function memberNo (error)
class + classNo

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 global with export {} to extend window, globalThis, or ProcessEnv.
  • Keep augmentations in dedicated *.d.ts files and include them in tsconfig.
  • Remember type aliases cannot merge — use interfaces when you need extensibility.
  • Prefer narrow, well-documented augmentations over broad global changes.
Last updated June 29, 2026
Was this helpful?