FrontendPrep
Menu
Topics
Questions
Guides
Challenges
Soon
Back to TypeScript Questions
typescriptMedium

TypeScript: Declaration Files and Declaration Merging

Understand ambient type definitions (.d.ts), declaration merging, namespace/interface merging, and how to perform global module augmentation for third-party libraries.

TypeScript: Declaration Files and Declaration Merging

An essential TypeScript module management and configuration interview question is:

What are declaration files (.d.ts) in TypeScript, and how does declaration merging work? Give a concrete example of how to extend a third-party library's interface (e.g. Express Request object) using module augmentation.

When using JavaScript libraries in a TypeScript project, we need declaration files to provide autocomplete and type safety. Sometimes, we also need to append properties to these definitions at runtime, which is made possible through declaration merging.


1. What are Declaration Files (.d.ts)?

Declaration files contain only type declarations and no executable JavaScript code. Their purpose is to describe the shape of existing JavaScript libraries to the TypeScript compiler.

  • They compile to nothing in the final output.
  • They use the declare keyword to define variables, classes, or modules that exist globally at runtime.

2. Declaration Merging

Declaration merging is when the TypeScript compiler merges two or more separate declarations declared with the same name into a single definition.

Interface Merging

Unlike type aliases (which cannot be redeclared), multiple interfaces with the same name in the same scope are merged automatically:

interface User {
  name: string;
}
 
interface User {
  age: number;
}
 
// Resulting User interface has both properties:
const person: User = {
  name: "Alice",
  age: 30
};

3. Namespace and Module Augmentation

Sometimes you need to augment definitions that reside inside external modules (like express or react). Since these modules are isolated, standard interface declarations won't merge with them directly. You must use module augmentation.

Real-world Example: Extending Express Request

Suppose your authentication middleware decodes a JWT and attaches the currentUser object to the Express Request object. By default, TypeScript will flag this with an error because currentUser does not exist on Express's Request interface:

// ❌ Compile Error: Property 'currentUser' does not exist on type 'Request'.
app.use((req, res, next) => {
  req.currentUser = { id: 1, role: "admin" }; 
  next();
});

To fix this, create a local declaration file (e.g., src/types/express.d.ts) to augment the module:

// src/types/express.d.ts
import { User } from './auth'; // import custom types if needed
 
declare global {
  namespace Express {
    interface Request {
      currentUser?: {
        id: number;
        role: string;
      };
    }
  }
}

Or for a standard module-based library definition:

// Augmenting session types inside standard libraries
import 'express-session';
 
declare module 'express-session' {
  interface SessionData {
    userId: string;
  }
}

How this works:

  1. The declare module 'library-name' syntax tells TypeScript we are appending types to the existing module.
  2. Interface declarations nested inside are merged with the library's original interface definitions.

Senior-Level Interview Answer

Declaration files (.d.ts) act as compile-time metadata describing JavaScript interfaces to the compiler without emitting runtime code. Declaration merging occurs when the compiler combines identical-named interfaces, namespaces, or classes into a unified construct. When extending third-party module interfaces (such as adding authenticated user fields to Express request items), standard global scope interfaces fail because of module isolation. We resolve this by declaring module augmentation using declare module 'module-name' or declare global blocks. Inside these blocks, we redeclare target interfaces, triggering interface merging, which expands the compiler's type registry for those library modules.


Common Interview Mistakes

❌ Forgetting import/export statements in augmentation files

If your augmentation file (express.d.ts) contains no import or export statements, TypeScript treats it as a global script file instead of a module. In module augmentation files, you must include at least one import or export statement (even an empty export {}) to force the compiler to parse it as an ES module and enable module augmentation.

❌ Attempting declaration merging with Type Aliases

Assuming you can write type User = { name: string } and then declare type User = { age: number }. Type aliases are unique, static declarations in the compiler registry and will throw a Duplicate identifier 'User' compilation error. Merging only works with interfaces and namespaces.


Key Takeaways

  • Type Metadata: .d.ts files contain type definitions that are compiled away and emit zero JavaScript at runtime.
  • Interface Merging: Multiple interfaces declared with the identical name in the same namespace merge their fields.
  • Module Augmentation: Use declare module 'name' to add properties to external libraries.
  • Module Trigger: Augmentation files require at least one import/export statement to be parsed as modules.
  • Type Limitation: Declaration merging is limited to interfaces and namespaces; type aliases cannot be merged.

Share this Resource

Help other developers level up by sharing this study guide.

More Technical Questions

Expand your mastery. Deep dive into other frontend interview challenges in this category.