Declaration Merging

In TypeScript, some declarations with the same name don't clash — they combine. Two interfaces called User become one interface with both sets of properties. This "declaration merging" is how libraries let you extend their types, and how you add properties to built-ins like Window.

Merging interfaces

TS interfaces.ts
interface Box {
  width: number;
}

interface Box {
  height: number;
}

const b: Box = { width: 10, height: 4 };
console.log(b.width * b.height);
Output
40

Both declarations contribute properties, and the result requires all of them. If both declare the same property, the types must be identical — otherwise it's an error. Methods with the same name become overloads, with later declarations taking priority.

Type aliases do not merge. Declaring type Box twice is a "duplicate identifier" error. This is the main practical difference between interface and type.

Extending built-in types

Because the standard library defines types like Window and Array as interfaces, you can add to them. A classic case is a global set by a script tag:

TS window.ts
declare global {
  interface Window {
    appConfig?: { apiUrl: string };
  }
}

function apiUrl(): string {
  return globalThis.window?.appConfig?.apiUrl ?? "http://localhost:3000";
}

console.log(apiUrl());
Output
http://localhost:3000

The declare global { … } block is required because this file is a module. In a plain global .d.ts file (no imports or exports), you can write interface Window { … } directly.

Module augmentation

The same idea works for third-party modules: you can reopen a module's declarations and add to them. Express projects commonly attach the logged-in user to the request object:

D.TS types/express.d.ts
import "express";

declare module "express-serve-static-core" {
  interface Request {
    user?: { id: string; role: "admin" | "member" };
  }
}

After this, req.user is typed in every route handler. The import at the top matters: it makes the file a module, so declare module augments the existing declarations instead of replacing them.

Many libraries are designed around this. Vue lets you augment ComponentCustomProperties, Pinia, i18n libraries and theme systems (such as styled-components' DefaultTheme) all ask you to merge your own types into an empty interface they export.

Merging namespaces with functions, classes and enums

A namespace can merge with a function, class or enum of the same name to add static-looking members:

TS namespace-merge.ts
enum Color {
  Red = "red",
  Green = "green",
}

namespace Color {
  export function parse(value: string): Color | undefined {
    return Object.values(Color).find(c => c === value) as Color | undefined;
  }
}

class Money {
  constructor(public cents: number) {}
}

namespace Money {
  export const zero = new Money(0);
}

console.log(Color.parse("green"), Money.zero.cents);
Output
green 0

This pattern appears in older libraries; in new code, a static method on a class or a separate helper function is usually clearer.

Interfaces merging with classes

An interface with the same name as a class adds members to the class's instance type, without adding any implementation. This is how mixins are typed:

TS class-merge.ts
class Article {
  constructor(public title: string) {}
}

interface Article {
  slug(): string;
}

Article.prototype.slug = function () {
  return this.title.toLowerCase().replace(/\s+/g, "-");
};

console.log(new Article("Declaration Merging 101").slug());
Output
declaration-merging-101
Careful: merging an interface into a class tells TypeScript that the method exists, but doesn't create it. If the prototype assignment were missing, the code would compile and then crash. Use this only when something else really does supply the implementation.

What can merge with what

InterfaceNamespaceClassEnumFunctionType alias
Interface✓✓✓✗✗✗
Namespace✓✓✓✓✓✗

Accidental merging

Merging can surprise you. If a global script file declares interface User and another global file does too, they silently combine into one interface. Keeping code in modules (files with import/export) avoids this, because each module's declarations are private to it.