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.
| Aspect | Namespace | ES Module |
|---|---|---|
| Dependency tracking | Implicit / global | Explicit import |
| Scope | Shared (merged) global | Per-file |
| Tree-shaking | Poor | Excellent |
| Tooling / ecosystem | Legacy | Standard |
| Recommended for app code | No | Yes |
Do not use namespaces to organize application source files. Reach for ES module
import/exportinstead. 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.tsfiles 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
exportdeliberately; unexported members are private.