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
interface Box { width: number; } interface Box { height: number; } const b: Box = { width: 10, height: 4 }; console.log(b.width * b.height);
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:
declare global { interface Window { appConfig?: { apiUrl: string }; } } function apiUrl(): string { return globalThis.window?.appConfig?.apiUrl ?? "http://localhost:3000"; } console.log(apiUrl());
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:
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:
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);
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:
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());
declaration-merging-101
What can merge with what
| Interface | Namespace | Class | Enum | Function | Type 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.