skip to content

In TypeScript, what does declaring a `namespace` with the same name as an existing interface, class, or function give you, and what ordering rule applies?

level: middleimportance: nice to knowfreq 30%

answer

  1. different declaration spaces, one name
  2. type on one side, values on the other
  3. the companion-object pattern
  4. export or it stays private
  5. the namespace has to come second

basics

~20 s

The namespace merges with the other declaration, so one name serves as both a type and a container of values — a companion for an interface, or static members hung on a class, function or enum. The namespace must come after the declaration it merges with.

solid answer

~50 s

Declarations occupy different spaces — an interface lives in the type space, a function or class in the value space — so a `namespace` of the same name merges rather than collides. Paired with an interface it gives you the companion-object pattern: `Point` is a type and also a value with `Point.origin` on it. Paired with a class, function or enum it attaches members to the value side, which is how you type a function that also carries properties. Two rules matter. Only `export`ed members of the namespace are reachable through the merged name; the rest stay private to the block. And when merging with a class, function or enum, the namespace declaration must follow it, because the namespace's emitted code assigns onto an existing binding. A namespace containing only types emits nothing; one containing values emits real code.

code

typescript · 14 lines
typescript
interface Point {
  x: number;
  y: number;
}

namespace Point {
  export const origin: Point = { x: 0, y: 0 };
  export function equals(a: Point, b: Point): boolean {
    return a.x === b.x && a.y === b.y;
  }
}

const p: Point = Point.origin;
console.log(Point.equals(p, { x: 0, y: 0 }));

go deeper

for a junior

Recall that a namespace can share a name with an interface, class or function and that only exported members of the namespace are reachable through that name.

for a middle

Explain the declaration-space reasoning behind the merge, the companion-object pattern, and the rule that a namespace must follow the class, function or enum it merges with.

for a senior

Distinguish namespaces-as-modules (legacy, replaced by ES modules) from namespaces-as-merging (still idiomatic), and be clear about which forms emit runtime code.

for a principal

Own the API-shape call: giving one exported name both a type and a value role is a convenience with real costs for tooling and tree-shaking, so decide deliberately when a companion namespace beats two plainly named exports.

## Declaration spaces are what make this work Every TypeScript declaration contributes to one or more of three spaces: **type**, **value**, and **namespace**. An `interface` contributes a type only. A `function` or `const` contributes a value only. A `class` contributes both a type (its instance type) and a value (its constructor). A `namespace` contributes a value — the object it emits — and a namespace-space entry for anything declared inside it. Two declarations collide only when they claim the same space. That is why a namespace can share a name with an interface, a class, a function or an enum: the pieces slot into different spaces and the compiler merges them into one symbol. ## Namespace plus interface: the companion pattern ```ts interface Point { x: number; y: number; } namespace Point { export const origin: Point = { x: 0, y: 0 }; export function equals(a: Point, b: Point): boolean { return a.x === b.x && a.y === b.y; } } const p: Point = Point.origin; // Point used as a type and as a value ``` The interface supplies the type; the namespace supplies helpers under the same name. Callers get one import and one concept instead of `Point` and `PointUtils`. This is a common way to package a type with its constructors and predicates. ## Namespace plus class, function or enum Merging a namespace with a class adds members to the class's *static* side and lets you nest types under the class name. Merging with a function attaches properties to the function value — the type-layer counterpart of the everyday JavaScript habit of hanging fields on a function object. Merging with an enum adds helpers alongside the enum members. Here an ordering rule applies: **the namespace must follow the class, function or enum it merges with.** The reason is emit. A namespace containing values compiles to an immediately-invoked function that augments an existing binding, so that binding must already exist when the namespace's code runs. Interfaces are exempt because they emit nothing at all, so there is no runtime ordering to respect. ```ts function greet(name: string): string { return `hello, ${name}`; } namespace greet { // must come after the function export const locale = 'en'; } console.log(greet('Ada'), greet.locale); ``` ## Only exported members are visible A namespace's members are private unless declared with `export`. A non-exported `const` inside the block is a genuine implementation detail — usable by the other members of that namespace, invisible through the merged name. When two namespace declarations of the same name merge, the same rule governs what each can see of the other: exported members are shared, non-exported ones stay local to their own block. ## What is emitted This is where the tree's central rule shows up. A namespace containing only types is erased completely — nothing reaches the output. A namespace containing values emits an object and assignments into it, so it is one of the few type-layer-looking constructs that has a runtime footprint, along with `enum`. Merging with an interface therefore costs nothing at runtime unless you put values in the namespace. ## What never merges Not every pairing is legal, and it is worth being precise: - **Class with class** — a duplicate identifier. Both claim the value space and the type space. - **Type alias with anything of the same name in the type space** — a duplicate identifier; aliases are closed. - **Function with function**, absent overload syntax — two implementations of the same name collide. And one pairing that does merge but is easy to overlook: an `interface` declaration merges with a **class** of the same name, contributing members to the class's instance type. That is the mechanism the mixin pattern relies on to tell the checker about members added dynamically. ## Judgment Namespaces used as a *module system* are legacy — modern TypeScript code organises code with ES modules, and a file-level `namespace` wrapper adds nesting for no gain. Namespaces used for *merging*, though, remain the idiomatic way to express "this name is both a type and a small set of values", and there is no ES-module equivalent that gives one identifier both roles. In a module you can achieve much of the companion effect by exporting a type and a const object of the same name from the same file, since a type and a value can share a name without conflict — but nested types under a value name still need the namespace form.

  • Why must a namespace that merges with a function be declared after it, while one merging with an interface can appear anywhere?
    Because of emit. A namespace holding values compiles to code that assigns onto an existing binding, so the function must already be declared when that code runs. An interface emits nothing, so there is no runtime binding and no ordering constraint — the merge is purely a checker-level operation.
  • Does merging a namespace into an interface add anything to the emitted JavaScript?
    Only if the namespace contains values. A namespace holding just types is erased entirely, like the interface itself. A namespace holding a `const` or `function` emits an object and the assignments into it, which is why namespaces — along with enums — are among the few constructs in the type layer that survive compilation.
  • Can two classes with the same name merge the way two interfaces do?
    No — that is a duplicate identifier, because both declarations claim the value space and the type space. What does work is an `interface` declaration merging into a class's instance type, which the mixin pattern uses to tell the checker about members attached at runtime, and a `namespace` merging into the class's static side.

saying these in an interview costs you the question

  • Thinks a namespace next to a class is always a duplicate identifier
  • Forgets export and wonders why the member is invisible
  • Declares the namespace before the function it merges with
  • Believes namespaces always disappear from the output
  • Says two same-named classes merge like interfaces

context