Suggest an editImprove this articleRefine the answer for “Declaration merging in TypeScript”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**Declaration merging in TypeScript** is when the compiler automatically combines multiple declarations of the same name into one definition. Interfaces support this; type aliases do not. ```typescript interface User { name: string; } interface User { age: number; } // Result: { name: string; age: number } ``` **Key:** Use interfaces for extensible types, type aliases for fixed shapes and unions.Shown above the full answer for quick recall.Answer (EN)Image**Declaration merging** in TypeScript is the compiler's ability to combine multiple declarations of the same name into a single definition. Interfaces, namespaces, and enums support it. Type aliases do not. ## Theory ### TL;DR - `interface User {}` declared twice merges automatically; `type User = {}` declared twice fails with a duplicate identifier error - Think of interfaces as a shared doc anyone can append to, and types as a locked file nobody can edit - Use interfaces for public APIs and library types; types for unions, primitives, and fixed shapes - Module augmentation (extending third-party types like Express or Jest) relies entirely on this feature - No runtime cost: all type information is erased before JavaScript output ### Quick example ```typescript interface User { name: string; } interface User { age: number; } interface User { email: string; } // TypeScript sees one merged type: // { name: string; age: number; email: string } const user: User = { name: "Alice", age: 25, email: "alice@example.com" }; // ✅ // This fails immediately: // type User = { name: string }; // type User = { age: number }; // ❌ Duplicate identifier 'User' ``` Three separate declarations become one. The compiler requires all properties at once. ### Interface merging vs type aliases Interfaces generate structural symbols that the compiler can intersect across files. Type aliases create opaque identifiers with no merge logic. This is intentional: interfaces are designed to be open and extensible, while type aliases represent a closed, final shape. For library code, the difference is significant. If you ship an interface, consumers can extend it without touching your source. If you ship a type alias, they cannot. Many teams default to interface for object shapes; others use type everywhere. Both approaches work. The choice comes down to whether you need extension (interface) or composition (type). ### When to use declaration merging - **Building a library or SDK**: declare props as interfaces so consumers can augment them without forking your package - **Module augmentation**: patch third-party types like Express `Request` or Jest `Matchers` - **Environment variables**: extend `NodeJS.ProcessEnv` with your specific env keys - **Namespace organization**: split large namespaces across files while keeping them under one name - **Union types, primitives, fixed shapes**: use `type` here since merging adds no value and a closed shape prevents accidental extension ### Merging rules | Declaration | Can merge with | |---|---| | `interface` | `interface` | | `namespace` | `namespace`, `class`, `function`, `enum` | | `class` | `namespace` | | `function` | `namespace` | | `enum` | `enum`, `namespace` | | `type` alias | nothing | Enums can merge with other enums, but each value must have a unique key. Conflicts error at compile time. ### How the compiler handles this TypeScript scans all `.ts` files in the compilation unit, collects declarations by exact name (case-sensitive), and merges compatible ones into a single symbol table entry during semantic analysis. For interfaces, it intersects member types: if two declarations define the same property with incompatible types, the compiler errors at the conflicting declaration. No runtime overhead exists since types are fully erased before JavaScript output. ### Module augmentation This is where declaration merging does its heaviest lifting in real projects. The Express pattern is the most common example: ```typescript // types/express.d.ts (no import/export = ambient script = global scope) declare namespace Express { interface Request { user?: { id: string; role: "admin" | "editor" | "viewer"; }; } } // Now TypeScript knows about req.user in every handler app.get("/profile", (req, res) => { console.log(req.user?.id); // ✅ fully typed }); ``` In an ES module file (one that has `import` or `export` at the top), you need `declare global` to reach the global scope: ```typescript // This file has imports, so it is a module import express from "express"; // ✅ declare global wraps the augmentation correctly declare global { namespace Express { interface Request { userId: string; } } } ``` Without `declare global`, the compiler treats the namespace as module-scoped and the augmentation applies to nothing. That is the most common bug with this pattern. ### Namespace merging Namespaces merge like interfaces, but exported functions with the same name must have compatible signatures: ```typescript namespace MyLib { export function log(msg: string): void { console.log("v1:", msg); } } namespace MyLib { // ❌ Error: incompatible with the signature above export function log(msg: string, level?: number): void { console.log("v2:", msg, level); } } ``` The fix is declaring overloads in one block: ```typescript namespace MyLib { export function log(msg: string): void; export function log(msg: string, level: number): void; export function log(msg: string, level?: number): void { console.log(msg, level); } } ``` This pattern appears frequently in `@types/node`, particularly for `fs.promises`. ### Common mistakes **Expecting `type` to merge like `interface`** ```typescript type Point = { x: number }; type Point = { y: number }; // ❌ Duplicate identifier 'Point' ``` Type aliases are closed by design. Switch to `interface` if you need merging, or use intersection: `type Point = { x: number } & { y: number }`. **Merging incompatible member types** ```typescript interface Box { width: number; } interface Box { width: string; // ❌ Subsequent property declarations must have the same type } ``` The compiler requires exact type matches for the same property across merged declarations. If you need different shapes, use intersection types instead. **Forgetting `declare global` in ES modules** ```typescript // This file has an import, so it is a module import express from "express"; // ❌ This augments nothing - namespace is module-scoped declare module "express" { interface Request { userId: string; } } // ✅ This works declare global { namespace Express { interface Request { userId: string; } } } ``` A `.ts` file with no imports or exports is an ambient script and augments the global scope automatically. Add any import and it becomes a module. **Enum value conflicts** ```typescript enum Status { Active = 1, } enum Status { Active = 2, // ❌ Redeclaration of 'Active' with a different value } ``` Each declaration in a merged enum must use unique keys. ### Real-world usage - **DefinitelyTyped (@types/react)**: merges `React.JSX.IntrinsicElements` so custom element attributes can be added per project - **Express middleware**: `declare global { namespace Express { interface Request { user?: User } } }` gives `req.user` across the whole app - **Node.js type shims**: augments the `global` namespace for browser polyfills like `process.browser` - **Jest custom matchers**: `declare module "@jest/expect" { interface Matchers<R> { toBeWithinRange(a: number, b: number): R } }` - **Vite env types**: `interface ImportMetaEnv` is augmented in `vite-env.d.ts` per project for typed `import.meta.env` One thing that trips up teams regularly: the augmentation file has `import` at the top, so it becomes a module, and the namespace never reaches the global scope. They spend an hour checking their code before realizing the file structure is the issue. ### Follow-up questions **Q:** Why does `interface` support merging but `type` does not? **A:** Interfaces generate structural symbols that the compiler can intersect across declarations. Type aliases create opaque references with no merge logic. Interfaces are designed to be open for extension; type aliases are designed to be final. **Q:** Can classes participate in declaration merging? **A:** Classes cannot merge with other classes. But a class can merge with a namespace, which lets you attach static utilities or factory functions without modifying the class body itself. **Q:** What happens when merged interface properties have incompatible types? **A:** The compiler errors at the point of the conflicting declaration, not at usage. The error message names the property and shows both types. **Q:** Does file order matter when merging? **A:** For interfaces, no. The compiler scans all files holistically and the order of declarations does not affect the merged shape. For function overloads inside a namespace, the order of signatures does matter for resolution. **Q:** How does declaration merging behave in ES modules vs CommonJS? **A:** The module system does not affect how the compiler merges declarations. What matters is whether a `.ts` file is ambient (no imports or exports) or a module. For module files, use `declare global` to reach the global scope. **Q:** (Senior) How does TypeScript 5.x handle triple-slash augmentation vs `package.json` `typesVersions`? **A:** Triple-slash directives for augmentation are still supported but the recommended approach for library authors is `typesVersions` in `package.json`. It maps type entry points per TypeScript version without adding directives to the global namespace or requiring consumers to reference files manually. ## Examples ### Design system component props split across files This pattern shows how a component library can distribute prop declarations across feature files without a single large type file that every team member edits: ```typescript // button.types.ts - base props interface ButtonProps { variant: "primary" | "secondary"; onClick: () => void; } // button.size.ts - adds sizing without touching the base file interface ButtonProps { size: "small" | "medium" | "large"; } // button.icon.ts - icon support is optional interface ButtonProps { icon?: string; iconPosition?: "left" | "right"; } // All three merge. TypeScript enforces all required props: function Button({ variant, size, onClick, icon }: ButtonProps) { // ... } // ✅ variant, size, onClick are required; icon is optional Button({ variant: "primary", size: "large", onClick: () => {} }); ``` Each team can own their file. No merge conflicts in version control. The compiler assembles the full type from all three. ### Express authentication middleware with typed `req.user` ```typescript // src/types/express.d.ts // No import/export here - this is an ambient script, so it's global declare namespace Express { interface Request { user?: { id: string; email: string; role: "admin" | "editor" | "viewer"; }; } } // src/middleware/auth.ts import { Request, Response, NextFunction } from "express"; export function requireAuth(req: Request, res: Response, next: NextFunction) { const token = req.headers.authorization?.split(" ")[1]; if (!token) { return res.status(401).json({ error: "Unauthorized" }); } // Token verified - attach user to request req.user = { id: "123", email: "user@example.com", role: "admin" }; next(); } // src/routes/profile.ts app.get("/profile", requireAuth, (req, res) => { res.json({ id: req.user?.id, role: req.user?.role }); // ✅ fully typed }); ``` The `.d.ts` file has no imports, so it is ambient and applies globally. Add `import express from "express"` to the top and the augmentation breaks unless you wrap it in `declare global { namespace Express { ... } }`. ### Namespace merging with overloads and a compiler error walkthrough This example shows the correct and incorrect ways to extend a namespace with a function that has multiple call signatures: ```typescript // ❌ Wrong: separate declarations with conflicting signatures namespace Logger { export function log(msg: string): void { console.log(msg); } } namespace Logger { // Error: Property 'log' is incompatible between declarations export function log(msg: string, level: "info" | "warn"): void { console.log(`[${level}]`, msg); } } // ✅ Correct: declare all overloads in one block namespace Logger { export function log(msg: string): void; export function log(msg: string, level: "info" | "warn"): void; export function log(msg: string, level?: "info" | "warn"): void { if (level) { console.log(`[${level}]`, msg); } else { console.log(msg); } } export interface Config { prefix?: string; defaultLevel: "info" | "warn"; } } // A second namespace block can still add non-conflicting members namespace Logger { export function createPrefixed(prefix: string): (msg: string) => void { return (msg) => Logger.log(`${prefix} ${msg}`); } } Logger.log("Server started"); // ✅ Logger.log("Auth failed", "warn"); // ✅ const apiLog = Logger.createPrefixed("API"); // ✅ apiLog("Request received"); // ✅ logs: API Request received ``` The compiler errors on function name conflicts between namespace declarations because it cannot determine which implementation to use. Non-conflicting members (like `createPrefixed` and `Config`) merge without issues.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.