skip to content

In TypeScript, what happens when the same interface name is declared twice in one scope, and what happens when the same `type` alias name is declared twice?

level: juniorimportance: must knowfreq 72%

answer

  1. one is open, one is closed
  2. same name, twice, same scope
  3. the compiler unions the members
  4. aliases get a duplicate-identifier error
  5. scope decides, not file count

basics

~20 s

Two interface declarations sharing a name in the same scope merge into one type carrying both sets of members. Repeating a type alias name is a duplicate-identifier error instead: interfaces are open, aliases are closed.

solid answer

~40 s

Interfaces are open. If I write `interface User { id: string }` and later, in the same scope, `interface User { name: string }`, TypeScript merges them into a single `User` that requires both `id` and `name` — no error, no last-one-wins. A `type` alias is closed: declaring `type User = ...` twice in the same scope is `Duplicate identifier 'User'`. Merging only happens inside one declaration space, so two different modules that each export a `User` interface stay completely independent types. That openness is not a quirk — it is the mechanism behind `declare global` and module augmentation, and it is the one capability a type alias structurally cannot offer.

code

typescript · 15 lines
typescript
interface User {
  id: string;
}

interface User {
  name: string;
}

// Both declarations contribute members.
const u: User = { id: 'u1', name: 'Ada' };

type Point = { x: number };
// type Point = { y: number }; // TS2300: Duplicate identifier 'Point'.

console.log(u.id, u.name);

go deeper

for a junior

Be able to say plainly that interfaces with the same name merge their members while a repeated type alias is a duplicate-identifier error, and give the two-line example.

for a middle

Explain that merging is scoped to one declaration space — same file scope, same namespace, or a deliberate augmentation — and that properties must agree in type while methods become overloads.

for a senior

Show judgment about openness as an API property: pick an interface when outside code must extend the shape, an alias when the shape must stay fixed, and be ready to say how you would trace an unexpected merged-in member.

for a principal

Own the tradeoff at codebase scale: openness makes types extensible but makes provenance hard, and merged members assert things the runtime never checks. Decide where your published types are reopenable at all.

## Two declarations, one type A TypeScript interface is *open* (sometimes called *reopenable*). When the compiler sees more than one `interface` declaration with the same name in the same scope, it does not treat the second as a redefinition or as an error — it merges them into a single interface whose member list is the union of all the declarations. ```ts interface User { id: string } interface User { name: string } // User is now { id: string; name: string } const u: User = { id: 'u1', name: 'Ada' }; // ok const bad: User = { id: 'u1' }; // error: 'name' is missing ``` Order does not matter, and neither declaration "wins": both contribute. Nothing is emitted for either one — interfaces are a pure type-layer construct that disappears at compile time, so merging is entirely a bookkeeping operation in the checker. ## Type aliases are closed A `type` alias introduces a name into the type declaration space exactly once. Declaring it twice in the same scope produces `Duplicate identifier` (TS2300) on both declarations: ```ts type Point = { x: number }; type Point = { y: number }; // error TS2300: Duplicate identifier 'Point'. ``` The compiler does not union them, intersect them, or take the last one. This is the same rule that applies to two `const`s or two classes with the same name; interfaces are the special case, not aliases. The practical consequence: if a shape must stay extensible by code you do not control, it has to be an interface. If you want a name that is definitively fixed — nobody, anywhere in the program, can quietly add a member to it — an alias gives you that guarantee. ## "Same scope" is doing real work Merging happens within one *declaration space*, not across the whole program. Concretely: - Two interfaces at the top level of the same **script** file (a file with no top-level `import`/`export`) merge, and they merge with any other global declaration of that name — that is how `lib.dom.d.ts`'s `Window` can be extended. - Two interfaces at the top level of the same **module** file merge with each other, but they are scoped to that module. A `User` interface in `a.ts` and a `User` interface in `b.ts` are unrelated types that happen to share a name; each module has its own declaration space. - Two interfaces inside the same `namespace` block, or in two `namespace` blocks with the same name, merge — namespaces themselves merge, and their contents merge with them. So merging is never "spooky action across the codebase" by accident. To reach into another module's declaration space on purpose you have to say so explicitly, with a `declare module 'pkg'` augmentation or a `declare global` block — both of which work precisely *because* interfaces merge. ## What merging does to members The merged interface is not a free-for-all. Ordinary (non-method) properties must either be unique across the declarations or be declared with an identical type; declaring `count: number` in one and `count: string` in another is an error. Method-syntax members with the same name are legal in several declarations and become an overload set rather than a conflict. So merging adds members; it cannot silently reinterpret one. ## Why this design TypeScript describes JavaScript, and JavaScript objects are extended after the fact all the time: a polyfill adds a method to `Array.prototype`, a plugin hangs a property on `window`, middleware attaches a field to a request object. An open interface is the type-layer counterpart of that reality — the description of a value can be extended by whoever extends the value. Aliases exist for the other job: naming an exact shape, a union, a tuple, a function type, a computed type. Unions, tuples and primitives cannot be expressed by an interface at all, and merging cannot be expressed by an alias at all. ## The cost to be honest about Openness cuts both ways. Because any file in the compilation can reopen an interface, a property may appear on a type with no declaration anywhere near where it is used, and a reader has to search the whole program to find who added it. And merging is a claim about types only — it changes what the checker believes, never what exists at runtime. Declaring `interface Window { analytics: Tracker }` does not create `window.analytics`; if the script that sets it never loads, the compiler will still happily let you call it and you get a runtime `TypeError`.

  • If merging only adds members, can a merged declaration make an existing required property optional?
    No. Merging contributes members; it never relaxes or rewrites one. Re-declaring an existing property with a different type — including adding `?` — is an error, because non-method members across merged declarations must be identical in type. To get a version with optional fields you build a separate type from it, for example with `Partial`, rather than reopening the interface.
  • Two modules each export an interface called `Config`. Does importing both cause a merge or a conflict?
    Neither. Each module has its own declaration space, so the two `Config` interfaces are simply unrelated types. At the import site you would hit a local naming collision and have to rename one with `as`, but nothing merges. Cross-module merging only happens when you deliberately write a module augmentation targeting that module's specifier.
  • Does a merged interface emit anything different in the JavaScript output?
    No. Interfaces are erased entirely, whether there is one declaration or five — the emitted JavaScript contains nothing for any of them. Merging is a compile-time bookkeeping step in the checker only, which is also why it costs nothing at runtime and provides no runtime guarantee that the merged-in members actually exist.

saying these in an interview costs you the question

  • Says the second interface declaration overwrites the first
  • Claims duplicate interfaces are an error like duplicate aliases
  • Thinks two same-named interfaces in different modules merge
  • Believes merging changes the emitted JavaScript
  • Says a merged property can be re-typed by the later declaration

context