Skip to content

TypeScript Declaration Merging: Examples & Common Errors

Learn TypeScript declaration merging with working interface and module augmentation examples, common compiler errors, and an interview exercise.

Rajesh DhimanFebruary 08, 20264 min read
On this page

What is declaration merging in TypeScript?

Declaration merging combines compatible declarations with the same name into one definition. For example, two interfaces named InterviewSession in the same scope describe a single interface containing members from both declarations. Module augmentation extends an existing module's types without editing its source.

For an interview, explain the distinction between describing a property and creating it: interface merging changes the type checker’s view, but does not assign values to JavaScript objects.

A working interface-merging example

Paste this example into a TypeScript file. Both fields are required when constructing the session:

export {};
 
interface InterviewSession {
  candidate: string;
}
 
interface InterviewSession {
  durationMinutes: number;
}
 
const session: InterviewSession = {
  candidate: 'Asha',
  durationMinutes: 45,
};
 
console.log(`${session.candidate}: ${session.durationMinutes} minutes`);
// Asha: 45 minutes

export {} keeps this example in module scope, so its names do not accidentally merge with global declarations in other files. Identically named interfaces in two separate modules do not automatically merge with each other.

Merging vs extends vs intersections

Technique Result Use it when
Repeated interface Session declarations One shared interface gains members An API intentionally allows extensions
interface TimedSession extends Session A new named interface Your application needs a specialized model
type TimedSession = Session & Timing A type combining requirements You need composition without reopening a declaration

Prefer a new type when only one feature needs the extra field. Merging affects every use of that interface in the program. For the broader tradeoffs, see types vs interfaces.

Conflicting properties are errors

Merging does not let a later declaration replace an earlier property type:

export {};
 
interface InterviewSession {
  durationMinutes: number;
}
 
interface InterviewSession {
  durationMinutes: string; // TS2717: subsequent property declarations must have the same type
}

Run this separately from the working example. Fix the disagreement at its source or introduce a different property. An intersection is not an override either: { durationMinutes: number } & { durationMinutes: string } requires an impossible number & string property, which becomes never.

Type aliases cannot be reopened through repeated declarations. Repeating type InterviewSession = ... produces a duplicate-identifier error. Interfaces are not the only declarations with merging rules; namespaces and some combinations involving functions, classes, and enums have additional rules covered in the TypeScript handbook.

Module augmentation: a complete example

Imagine a small interview client whose session model needs an optional score. This example uses a local module so you can run it without installing a third-party library.

Create these three files in an empty folder.

session.ts — the original module:

export interface Session {
  id: string;
}
 
export function createSession(id: string): Session {
  return { id };
}

session-augmentation.d.ts — the type extension:

import './session';
 
declare module './session' {
  interface Session {
    score?: number;
  }
}

main.ts — the runtime code:

import { createSession } from './session';
 
const session = createSession('practice-01');
console.log(session.score); // undefined: augmentation did not create a value
 
session.score = 8;
console.log(session.score.toFixed(1)); // 8.0

Use this tsconfig.json in the same folder:

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "CommonJS",
    "strict": true,
    "outDir": "dist"
  },
  "include": ["*.ts", "*.d.ts"]
}

With TypeScript installed, run npx tsc -p tsconfig.json. In a folder without a package declaring "type": "module", run node dist/main.js. The output is undefined, then 8.0.

The top-level import makes the declaration file a module and anchors the augmentation to the existing module. The .d.ts file contributes types; it emits no runtime assignment. Keeping score optional accurately represents a newly created session before scoring.

In a real application, use its existing module settings rather than replacing its configuration with this learning example. See the tsconfig guide for the surrounding configuration concepts.

Why is my augmentation not working?

Symptom Check Fix
Property does not exist Is the declaration file part of this TypeScript project? Inspect npx tsc -p tsconfig.json --listFiles; include the file in the intended project
Existing module exports seem to disappear Is this a standalone ambient declaration instead of augmentation? Add a top-level import of the existing module
Invalid module name in augmentation Does the module specifier resolve from the declaration file? Correct the relative path or use the installed library's documented module name
Conflicting property declarations Has another file already declared the property differently? Align the type and optionality; do not attempt to override it
The code compiles but the value is undefined Is runtime code actually assigning the property? Assign and validate the value where it is produced

For libraries such as Express, first identify the interface and module the installed version exposes for extension. Do not assume that augmenting a similarly named module changes the type used by your application.

Practice: predict the compiler result

Before reading the answer, decide which line fails and whether merging creates a default value:

export {};
 
interface Attempt {
  questionId: string;
}
 
interface Attempt {
  correct: boolean;
}
 
const attempt: Attempt = { questionId: 'ts-09' };

Answer and explanation

The assignment fails because correct is required in the merged interface. Fix it with { questionId: 'ts-09', correct: false }. Merging does not insert false automatically. Making the property optional would be appropriate only if “not answered yet” is a valid state in your model.

Explain your answer out loud, then continue with TypeScript configuration or return to the TypeScript roadmap.

Companion Book

Looking for structured, offline interview prep?

Free online lessons • Portable Amazon Kindle / Paperback

Frontend Interview Guide (2025 Edition) cover
Recommended for React & Frontend Rounds

Frontend Interview Guide (2025 Edition)

By Rajesh Dhiman

Comprehensive preparation covering React internals, Web Performance, CSS architecture, and system design questions.

View on Amazon ↗Kindle & Paperback
JavaScript Interview Guide cover
Recommended for Core JS Rounds

JavaScript Interview Guide

By Rajesh Dhiman

Deep dive into the Event Loop, microtask draining, closures, prototypal inheritance, and asynchronous execution patterns.

View on Amazon ↗Kindle & Paperback

Note: All lessons on Frontend Interview remain completely free to read online. As an Amazon Associate, we may earn from qualifying purchases.

RD

Written by Rajesh Dhiman

Authored by Rajesh Dhiman and engineered at Eunix Tech. Focused on deep-dive frontend engineering, JavaScript runtime internals, React architecture, and web performance.