Namespaces & Declaration Merging

10 questions found

A namespace groups related code under a single named container using the 'namespace' keyword, generating a nested object at runtime — it was TypeScript's original way to organize code before ES Modules became the standard, and is mostly used today for organizing types or in non-module global scripts.
namespace Validation {
  export function isValid(input: string): boolean {
    return input.length > 0;
  }
}
Validation.isValid('hello'); // true
Real-world example Organizing a set of related global type definitions in a non-module ambient .d.ts file.

Common follow-ups: Why do modern TypeScript style guides generally recommend ES Modules over namespaces?

Modules
Just like a module, everything declared inside a namespace is private to that namespace by default; only explicitly 'export'ed members become accessible via NamespaceName.memberName from outside code.
namespace MathUtils {
  function internalHelper() { return 1; } // not accessible outside
  export function add(a: number, b: number) { return a + b + internalHelper(); }
}
MathUtils.add(2, 3); // works
// MathUtils.internalHelper(); // Error: not exported
Real-world example Hiding internal helper functions within a namespace while exposing only its intended public API.

Common follow-ups: Can a namespace be nested inside another namespace?

Modules
When you declare an interface with the same name multiple times (even in different files), TypeScript automatically MERGES all their members into a single combined interface, rather than treating the second declaration as an error or an override — this is unique to interfaces (type aliases do NOT support this).
interface Window {
  myCustomProp: string;
}
interface Window {
  anotherProp: number;
}
// merged result: Window now has both myCustomProp AND anotherProp
Real-world example Extending a built-in global interface (like Window or Express's Request) with additional properties from a separate file.

Common follow-ups: Why does this same automatic merging NOT work with 'type' aliases?

Declaration Files
A namespace can merge with a class, function, or enum of the same name declared alongside it, effectively attaching static-like members or nested types to that class/function/enum — this pattern was TypeScript's original mechanism for expressing things like static-nested-types before more modern syntax existed.
class Album {
  title: string = '';
}
namespace Album {
  export function create(title: string): Album {
    const a = new Album();
    a.title = title;
    return a;
  }
}
Album.create('OK Computer'); // uses the merged namespace member
Real-world example Attaching a static factory-like namespace of helper functions directly onto a class, without those helpers being instance methods.

Common follow-ups: Is this class-namespace merging pattern still commonly recommended in modern TypeScript?

Modules
Declare a namespace with the same name as the enum, and add exported functions inside it — TypeScript merges the namespace's members onto the enum object, letting you attach utility functions (like validation or display formatting) directly to the enum's name.
enum Color { Red, Green, Blue }
namespace Color {
  export function isValid(value: number): boolean {
    return value in Color;
  }
}
Color.isValid(1); // true, uses the merged namespace function
Real-world example Adding a helper function like isValid() or toDisplayName() directly onto an enum's own namespace, rather than as a separate standalone function.

Common follow-ups: Would this same merging capability work if you tried to merge a namespace with a 'const enum' instead?

Enums
If both interface declarations declare the SAME property name with the SAME type, they merge without conflict; if they declare it with DIFFERENT, incompatible types, TypeScript raises a compile error — merging only works cleanly when the overlapping members are structurally compatible across all declarations.
interface Config { timeout: number; }
interface Config { timeout: number; retries: number; } // OK, 'timeout' matches
// interface Config { timeout: string; } // Error: subsequent property declaration must be of type 'number'
Real-world example Debugging a confusing 'subsequent property declarations must have the same type' error when two teams accidentally declare overlapping global interfaces differently.

Common follow-ups: How does interface merging handle overlapping METHOD signatures rather than plain properties, in terms of overload ordering?

Types & Interfaces
Combine 'declare module' (to reopen the library's module) with an interface declaration matching the class's name inside it — TypeScript merges your added interface members onto the library's own class type, so instances gain the new property/method in the type system (though you're still responsible for ACTUALLY implementing that behavior at runtime, e.g. via monkey-patching or a plugin system).
// dayjs-plugin.d.ts
import 'dayjs';
declare module 'dayjs' {
  interface Dayjs {
    fromNow(): string; // added by a plugin like dayjs/plugin/relativeTime
  }
}

// usage.ts
import dayjs from 'dayjs';
dayjs().fromNow(); // now type-checks correctly
Real-world example Writing plugin type augmentations for a library like Day.js, Vue, or Express whose plugins add real runtime methods to existing classes.

Common follow-ups: Why does the augmenting file need to import the original module before declaring the augmentation?

Declaration Files
Multiple files can each contribute a piece of the SAME namespace (using 'namespace Foo { ... }' repeated across files, all included in the compilation), and TypeScript merges all their exported members into one logical namespace — useful for splitting a large set of ambient global types across multiple organized files while still presenting a single unified namespace to consumers.
// file1.d.ts
namespace MyLib {
  export interface Config { name: string; }
}

// file2.d.ts
namespace MyLib {
  export interface Options { verbose: boolean; }
}
// Both Config and Options are accessible as MyLib.Config and MyLib.Options
Real-world example Organizing a large third-party library's ambient global type declarations across multiple logically-grouped files.

Common follow-ups: What build/inclusion mechanism ensures TypeScript sees and merges declarations spread across multiple .d.ts files like this?

Declaration Files
Type aliases are meant to bind a NAME to a single, specific type expression once — allowing the same name to be redeclared with a different type expression would be ambiguous and error-prone (which expression 'wins'?), so TypeScript intentionally disallows it and raises a 'Duplicate identifier' error, unlike interfaces which are explicitly designed around incremental, additive extension.
type Config = { name: string };
// type Config = { retries: number }; // Error: Duplicate identifier 'Config'

interface IConfig { name: string; }
interface IConfig { retries: number; } // OK: interfaces merge
Real-world example Deciding to use 'interface' instead of 'type' specifically because a type needs to support later extension via declaration merging, like a global augmentation target.

Common follow-ups: Given this difference, why do many style guides still recommend 'type' as the DEFAULT choice for most non-extensible type definitions?

Types & Interfaces
Convert each namespace block into a module file with normal 'export' statements (removing the namespace wrapper syntax), replace namespace-qualified references (NamespaceName.member) with proper import statements, and watch for previously-implicit global availability (namespaces often relied on being loaded via <script> tags in a specific order) that now needs explicit import/export wiring.
// Before: namespace-based (implicit global load order matters)
namespace Utils {
  export function formatDate(d: Date): string { /* ... */ }
}

// After: ES Module
// utils.ts
export function formatDate(d: Date): string { /* ... */ }
// consumer.ts
import { formatDate } from './utils';
Real-world example Modernizing a legacy TypeScript codebase (originally built before ES Modules were standard) to use proper module imports/exports.

Common follow-ups: Why might a namespace-to-module migration also require reordering or restructuring how files are bundled/loaded?

Modules