TypeScript Declaration Merging: Examples & Common Errors
Learn TypeScript declaration merging with working interface and module augmentation examples, common compiler errors, and an interview exercise.
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 minutesexport {} 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.0Use 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.
Looking for structured, offline interview prep?
Free online lessons • Portable Amazon Kindle / Paperback
Frontend Interview Guide (2025 Edition)
By Rajesh Dhiman
Comprehensive preparation covering React internals, Web Performance, CSS architecture, and system design questions.
JavaScript Interview Guide
By Rajesh Dhiman
Deep dive into the Event Loop, microtask draining, closures, prototypal inheritance, and asynchronous execution patterns.
Note: All lessons on Frontend Interview remain completely free to read online. As an Amazon Associate, we may earn from qualifying purchases.
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.