Right, so you’ve met interfaces and you think you know how they work. You define a shape, you use it, life is good. But then you stumble into a codebase and see the same interface name declared multiple times. Your first instinct is to panic, thinking some maniac has redeclared everything. Relax. This isn’t a bug; it’s one of TypeScript’s most powerful, and occasionally terrifying, features: declaration merging.

Think of it like this: an interface isn’t a final, sealed class. It’s more of an open invitation. Every time you use the same interface name in the same scope, TypeScript doesn’t throw an error. Instead, it politely takes all the definitions and merges them into a single, massive interface. It’s the language’s way of letting you extend shapes without formally using the extends keyword, which is incredibly useful for patching together definitions from different parts of your code—or, more commonly, for augmenting third-party libraries.

How the Merge Actually Works

The rules are straightforward but crucial. TypeScript performs a recursive merge of the members. If you define two interfaces with the same name, the final result is as if you had written one big interface with all the properties from both.

interface Car {
  make: string;
  year: number;
}

interface Car {
  model: string;
  electric: boolean;
}

// From TypeScript's perspective, it now sees ONE interface:
// interface Car {
//   make: string;
//   year: number;
//   model: string;
//   electric: boolean;
// }

const myCar: Car = {
  make: 'Tesla',
  year: 2023,
  model: 'Model 3', // This is now required
  electric: true     // This is now required
};

Notice that model and electric, from the second declaration, are now full members of the Car interface. The merge is intelligent, but it’s also a blunt instrument.

The Catch: Handling Conflicting Members

This is where the brilliance can quickly turn into a debugging nightmare. You can’t just merge anything. The rules are strict:

  1. Non-function members must be identical. If you declare a property name as string in one place and try to redeclare it as number in another, the compiler will rightly tell you that’s nonsense and throw an error. It can’t decide which type is correct.

  2. Function members are overloaded. This is the wild part. If you merge interfaces with the same method name, TypeScript creates an overload list. The order of precedence is counter-intuitive but important: declarations from later interfaces come first in the overload list.

interface Logger {
  log(message: string): void;
}

interface Logger {
  log(message: string, level: 'info' | 'error'): void;
}

// The merged interface effectively has these overloads, in this order:
// log(message: string, level: 'info' | 'error'): void;
// log(message: string): void;

// This means this call is handled by the second overload (one argument)
const a: Logger['log'] = (msg: string) => console.log(msg);
// This call is handled by the first overload (two arguments)
const b: Logger['log'] = (msg: string, level: 'info' | 'error') => console.log(`[${level}] ${msg}`);

This is incredibly powerful for building flexible APIs, but if you get the order wrong, you’ll get baffling type errors. The logic is that the more specific signature (with two parameters) should be checked before the more general one.

Why You’d Actually Use This (And When to Run)

The primary, and most legitimate, use case is ambient declaration merging—most notably, augmenting types from libraries you don’t control.

Let’s say you’re using a library that has a global Window interface but is missing a property you need for your browser plugin. Instead forking the library’s type definitions, you can just declare the interface yourself. As long as you’re in a global module (i.e., a file without any import/export statements), TypeScript will happily merge your additions.

// In a file like `window.d.ts` or in a global scope
interface Window {
  myCoolPlugin: {
    version: string;
    doSomethingAwesome: () => void;
  };
}

// Now, elsewhere in your app, this is perfectly valid
window.myCoolPlugin.doSomethingAwesome();

It feels like magic, and it is. It’s how type definitions for the DOM and popular libraries are often built and extended.

The best practice? Use this power sparingly and with immense caution in your own code. For the love of all that is holy, do not start scattering multiple declarations of User interface all over your application. You will lose track of what defines a User and your teammates will (rightfully) revolt. For your own interfaces, always prefer explicit extension with extends. It’s clearer, safer, and doesn’t rely on the order of your files to define the fundamental structure of your data. Use merging for what it’s best at: patching the outside world to fit your needs.