Skip to content
TypeScript ts modules 3 min read

Namespaces

Namespaces (originally “internal modules”) were TypeScript’s pre-ESM way to organize code and avoid polluting the global scope. Today, ES modules are the standard for application code, and namespaces are considered legacy for that purpose. They still have niche, legitimate uses — chiefly grouping types in global declaration files and modeling JavaScript libraries that expose nested objects. This page explains the syntax, contrasts namespaces with modules, and shows the few places they remain the right tool.

Namespace basics

A namespace wraps declarations in a named object, exposing only what you mark export. Everything else stays private to the namespace.

namespace Geometry {
  export interface Point {
    x: number;
    y: number;
  }

  const TAU = Math.PI * 2;

  export function circumference(r: number): number {
    return TAU * r;
  }
}

const p: Geometry.Point = { x: 1, y: 2 };
const c = Geometry.circumference(5);

Members are accessed with dotted paths (Geometry.Point). Unlike modules, a namespace does not require importing — it is visible wherever it is in scope.

Why modules replaced them

For organizing real source files, ES modules win on every axis: explicit dependencies, per-file scope, bundler tree-shaking, and ecosystem tooling. Namespaces rely on global merging, which makes dependencies implicit and bundling harder.

AspectNamespaceES Module
Dependency trackingImplicit / globalExplicit import
ScopeShared (merged) globalPer-file
Tree-shakingPoorExcellent
Tooling / ecosystemLegacyStandard
Recommended for app codeNoYes

Do not use namespaces to organize application source files. Reach for ES module import/export instead. The TypeScript handbook explicitly recommends modules over namespaces for modern code.

Nested and merged namespaces

Namespaces can be nested and split across declarations — they merge automatically, which is part of why they linger in type declarations.

namespace App {
  export namespace Utils {
    export function clamp(n: number, lo: number, hi: number): number {
      return Math.min(Math.max(n, lo), hi);
    }
  }
}

// declared again elsewhere — merges into the same namespace
namespace App {
  export const version = "1.0.0";
}

App.Utils.clamp(5, 0, 10);
console.log(App.version);

This open-ended merging is useful for describing libraries whose runtime shape is a deeply nested object.

Where namespaces still help

The main modern use is in ambient declaration files (.d.ts) to model the shape of global libraries — especially UMD globals or scripts loaded via <script> that attach a nested object to window.

// jquery.d.ts (illustrative)
declare namespace JQuery {
  interface AjaxSettings {
    url: string;
    method?: "GET" | "POST";
  }
}

declare function $(selector: string): JQueryStatic;

interface JQueryStatic {
  ajax(settings: JQuery.AjaxSettings): Promise<unknown>;
}

Namespaces also pair with declaration merging to add nested types to a function or class — for example, attaching an options interface under a function’s name.

Namespaces vs modules in one file

Avoid mixing namespace with import/export to structure the same concern. If a file is already a module (it has imports/exports), wrapping its contents in a namespace just adds a redundant layer.

// ❌ Redundant: file is already a module
export namespace Helpers {
  export function noop(): void {}
}

// ✅ Just export directly
export function noop(): void {}

Inside a module, prefer plain exports; let the module be the namespace.

Best Practices

  • Use ES modules (import/export) for all application source code.
  • Reserve namespaces for ambient .d.ts files describing global or UMD libraries.
  • Do not wrap module file contents in a namespace — it duplicates the module boundary.
  • Leverage namespace merging only when modeling a nested runtime object shape.
  • Migrate legacy namespace-based code to modules when you touch it.
  • Mark namespace members export deliberately; unexported members are private.
Last updated June 29, 2026
Was this helpful?