Skip to content
Skip to content
DocsTypeScript ExpertexpertDeclaration Merging
Chapter 6 of 10·expert·8 min read

Declaration Merging

Hợp Nhất Khai Báo

Extend interfaces, namespaces, and modules with declaration merging

Hover or tap any paragraph to see Vietnamese translation

What Is Declaration Merging?

TypeScript permits multiple declarations with the same name in the same scope, as long as they belong to compatible entity kinds. The compiler merges them into a single definition. This is purely a type-system feature — no JavaScript runtime construct is involved.

interface-merge-basics.ts
1// Three separate interface declarations…2interface Box {3  height: number;4}56interface Box {7  width: number;8}910interface Box {11  depth: number;12}1314// …merged into one15const cube: Box = { height: 10, width: 10, depth: 10 }; // all three required

Interface Merging

Interface merging is the most common case. TypeScript unions all members from every declaration with the same name. This is the mechanism for extending an existing interface without touching its source file.

interface-merging.ts
1// lib/types.ts — original declaration2interface PaginatedResponse<T> {3  data: T[];4  total: number;5  page: number;6}78// extensions/types.ts — added later, same name9interface PaginatedResponse<T> {10  cursor?: string;   // new optional field11  hasMore: boolean;  // new required field12}
Info
Declaration order affects overload priority for function members — later declarations take higher precedence. For non-function members the types must be identical across declarations; there is no implicit union.

Namespace Merging

Namespaces merge too. TypeScript combines all exported members from declarations with the same name. This is commonly used to split a namespace across files or add sub-types and constants to an existing one.

namespace-merging.ts
1// validators/core.ts2namespace Validators {3  export interface StringValidator {4    isAcceptable(s: string): boolean;5  }6}78// validators/zip.ts9namespace Validators {10  const numberRegexp = /^[0-9]+$/;  // not exported — private to this file1112  export class ZipCodeValidator implements StringValidator {13    isAcceptable(s: string): boolean {14      return s.length === 5 && numberRegexp.test(s);15    }16  }17}1819// Usage — both members visible20const v: Validators.StringValidator = new Validators.ZipCodeValidator();

Non-exported members of a namespace block are visible only within that block. This creates private namespace-level state shared among functions in the same file without leaking outward.

Module Augmentation

Module augmentation lets you extend third-party module types without forking or modifying node_modules. It is the standard technique for patching types onto libraries with incomplete or outdated definitions.

module-augmentation.ts
1// augment-axios.d.ts23// IMPORTANT: this file must be a module (not a script)4// so add at least one import or export5export {};67declare module "axios" {8  interface AxiosRequestConfig {9    // Inject a custom traceId into every request config10    traceId?: string;11    retries?: number;12  }
Info
The augmenting file must be a module — it needs at least one top-level import or export. Without it TypeScript treats the file as an ambient script and the declare module creates a brand-new module rather than extending the existing one.

Merging Interfaces with Classes

You can declare an interface with the same name as a class to add members to the class type. This is used to reconstruct "type-only mixins" or to safely annotate class members that decorators are known to inject.

class-interface-merge.ts
1class ApiClient {2  baseUrl: string;34  constructor(baseUrl: string) {5    this.baseUrl = baseUrl;6  }78  get(path: string): Promise<unknown> {9    return fetch(this.baseUrl + path).then((r) => r.json());10  }11}12

Practical: Augmenting Express and Next.js

Extending Express Request

The most common pattern in Express/Node projects is attaching a user property to the Request object after authentication.

express-augmentation.ts
1// src/types/express/index.d.ts2import type { User } from "../../models/user";34declare global {5  namespace Express {6    interface Request {7      user?: User;8      requestId: string;9    }10  }11}12

Extending Next.js Session

nextauth-augmentation.ts
1// types/next-auth.d.ts2import type { DefaultSession } from "next-auth";34declare module "next-auth" {5  interface Session {6    user: {7      id: string;8      role: "admin" | "editor" | "viewer";9    } & DefaultSession["user"];10  }1112  interface JWT {

Pitfalls and Limitations

Function Overload Ordering

When merged interfaces contain function members, TypeScript places overloads from later declarations first in the merged overload list. This reverses the order compared with standard function overloads and can produce unexpected resolution.

overload-order-pitfall.ts
1interface Formatter {2  format(value: number): string;  // declared first — resolves LAST3}45interface Formatter {6  format(value: string): string;  // declared later — resolves FIRST7}89// Effective merged signature (later declarations win higher priority):10// format(value: string): string;11// format(value: number): string;1213declare const f: Formatter;14const result = f.format(42);  // string — but the number overload is lower priority

Ambient Modules and Wildcard Modules

A declare module 'name' in a script-mode file (no imports/exports) creates a brand-new ambient module. This is powerful for typing non-TypeScript files but is easily confused with augmentation.

ambient-vs-augmentation.ts
1// ambient module for SVG imports (script-mode file — no imports)2declare module "*.svg" {3  const ReactComponent: React.FunctionComponent<React.SVGProps<SVGSVGElement>>;4  export default ReactComponent;5}67// wildcard for CSS modules8declare module "*.module.css" {9  const styles: Record<string, string>;10  export default styles;11}1213// augmentation (module-mode file — has export {})14export {};15declare module "some-lib" {16  interface ExistingInterface {17    newField: string;  // extends, not replaces18  }19}

Summary

  • Interface merging unifies all same-name declarations into one — no edits to the source file required
  • Namespace merging combines exported members from multiple same-name blocks
  • Module augmentation extends third-party types via declare module in a module-mode file
  • Merging an interface with a class safely types decorator-injected members with zero runtime cost
  • Later declarations get higher overload priority for function members — order matters
  • The augmenting file must be a module with at least one top-level import or export
  • Built: 6/25/2026, 3:03:36 PM