TypeScript Namespaces
A namespace in TypeScript is a way to group related types, interfaces, functions, and variables under a single named container, so their names don’t collide with other code in the global scope. Namespaces predate ES modules in the TypeScript world — they were originally called “internal modules” — and today they’re mostly used for organizing code in non-module scripts, writing ambient type declarations, or maintaining older codebases that don’t use a bundler. Understanding namespaces also helps you read a lot of legacy TypeScript and library .d.ts files.
Overview: How Namespaces Work
A namespace is declared with the namespace keyword followed by a name and a block of code. Anything inside that block is private to the namespace unless you mark it with export. From outside the namespace, you reach exported members through dot notation using the namespace’s name, similar to how you’d access a property on an object — because that’s essentially what a namespace compiles down to.
Namespaces exist purely at compile time as an organizational tool; like all TypeScript type information, anything type-only (interfaces, type aliases) is fully erased when compiled to JavaScript. But unlike interfaces, a namespace can also contain real runtime values — functions, classes, variables — so the compiler doesn’t erase the namespace itself. Instead, it compiles a namespace into a plain JavaScript object, built with an Immediately Invoked Function Expression (IIFE), and every exported member becomes a property on that object. This is why you can use a namespace to group runtime code, not just types.
One especially important property of namespaces is declaration merging: if you declare a namespace with the same name more than once (even across separate files, using triple-slash reference directives to link them), TypeScript merges all the declarations into a single namespace. This was historically how large codebases split a namespace’s implementation across many files before ES modules and bundlers became standard. It’s still how many .d.ts declaration files augment built-in or third-party types.
In modern TypeScript, the official recommendation is to prefer ES modules (import/export at the top level of a file) for application code, since they’re the JavaScript standard, work well with bundlers and tree-shaking, and avoid polluting the global scope. Namespaces remain useful for: scripts that run directly in a browser without a module loader or bundler, organizing large blocks of ambient type declarations, and namespacing UMD-style libraries. Note that once a file contains a top-level import or export statement, it becomes an ES module, and namespaces declared inside it behave as module-scoped exports rather than participating in global merging across the whole program.
Syntax
namespace NamespaceName {
export interface SomeType {
// type shared with consumers
}
export function someFunction(): void {
// exported runtime code
}
function helper(): void {
// private to the namespace, not accessible outside
}
}
| Part | Meaning |
|---|---|
namespace |
Keyword that starts the declaration. |
NamespaceName |
The identifier used to access exported members from outside (e.g. NamespaceName.someFunction()). |
export |
Makes a member visible outside the namespace. Without it, the member is private to the namespace body. |
nested namespace |
A namespace can contain another export namespace to build a dotted hierarchy, e.g. Outer.Inner.member. |
import X = A.B |
Creates a shorter local alias for a deeply nested namespace member. |
Examples
Example 1: A basic namespace
namespace Shapes {
export interface Point {
x: number;
y: number;
}
export function distance(a: Point, b: Point): number {
const dx = a.x - b.x;
const dy = a.y - b.y;
return Math.sqrt(dx * dx + dy * dy);
}
}
const p1: Shapes.Point = { x: 0, y: 0 };
const p2: Shapes.Point = { x: 3, y: 4 };
console.log(Shapes.distance(p1, p2));
Output:
5
The Point interface and distance function are both exported, so they’re accessible as Shapes.Point and Shapes.distance from outside the namespace body. Without export, neither would be visible at the call site.
Example 2: Nested namespaces
namespace App {
export namespace Validation {
export interface Validator {
isValid(value: string): boolean;
}
export class LettersOnlyValidator implements Validator {
isValid(value: string): boolean {
return /^[A-Za-z]+$/.test(value);
}
}
export class NumbersOnlyValidator implements Validator {
isValid(value: string): boolean {
return /^[0-9]+$/.test(value);
}
}
}
}
const validators: Record<string, App.Validation.Validator> = {
letters: new App.Validation.LettersOnlyValidator(),
numbers: new App.Validation.NumbersOnlyValidator(),
};
const testStrings = ["Hello", "12345", "Hello123"];
for (const s of testStrings) {
for (const name in validators) {
const isMatch = validators[name].isValid(s);
console.log(`"${s}" - ${name}: ${isMatch}`);
}
}
Output:
"Hello" - letters: true
"Hello" - numbers: false
"12345" - letters: false
"12345" - numbers: true
"Hello123" - letters: false
"Hello123" - numbers: false
Namespaces can be nested arbitrarily deep. To reach Validation‘s members from outside App, you need to mark both the outer namespace member (Validation) and the inner members as export — and you access them with the full dotted path, App.Validation.Validator.
Example 3: Namespace aliasing with import
namespace Shapes {
export namespace Polygons {
export class Triangle {
constructor(public base: number, public height: number) {}
area(): number {
return (this.base * this.height) / 2;
}
}
}
}
import Polygons = Shapes.Polygons;
const t = new Polygons.Triangle(6, 4);
console.log(t.area());
Output:
12
Deeply nested namespace paths like Shapes.Polygons.Triangle get verbose fast. The import Alias = A.B.C syntax (note: this is not an ES module import, despite the keyword) creates a local shorthand, letting you write Polygons.Triangle instead. This is purely a compile-time convenience — it doesn’t create a new object at runtime, just a reference to the existing one.
Under the Hood: Compilation and Declaration Merging
When TypeScript compiles a namespace, it wraps the body in an IIFE and reuses (or creates) a single object to hold the exported members. Conceptually, namespace Utils { export function double(n) {...} } compiles to something like var Utils; (function (Utils) { function double(n) { return n * 2; } Utils.double = double; })(Utils || (Utils = {}));. The Utils || (Utils = {}) pattern is the key to declaration merging: each time a namespace with the same name is compiled, it reuses the existing object instead of overwriting it, so multiple namespace Utils { ... } blocks (even in different files) all add properties to the same underlying object.
namespace Utils {
export function double(n: number): number {
return n * 2;
}
}
namespace Utils {
export function triple(n: number): number {
return n * 3;
}
}
console.log(Utils.double(4));
console.log(Utils.triple(4));
Output:
8
12
Both namespace Utils blocks merge into one namespace with both double and triple available. This is exactly how large pre-ES-module TypeScript codebases split a single logical namespace’s implementation across many files: each file re-opens the same namespace name, and a set of triple-slash /// <reference path="..." /> comments tells the compiler how to order and merge them, since the type checker still needs to see every declaration to resolve the merged shape correctly. It’s also worth remembering that all of this is a compile-time and type-checking concern — once emitted, the resulting JavaScript is just plain objects and functions; there is no trace of interfaces, type annotations, or the namespace keyword left in the output.
Common Mistakes
Mistake 1: Forgetting to export a member
namespace Config {
const apiUrl = "https://api.example.com";
}
console.log(Config.apiUrl);
This fails to compile with an error like Property 'apiUrl' does not exist on type 'typeof Config'. Members without export are private to the namespace body — they never become properties on the compiled namespace object, so nothing outside can see them, even though they’re declared with const at the “top level” of the namespace.
The fix is to add export:
namespace Config {
export const apiUrl = "https://api.example.com";
}
console.log(Config.apiUrl);
Output:
https://api.example.com
Mistake 2: Skipping the full path for nested namespaces
namespace App {
export namespace Utils {
export function greet(name: string): string {
return `Hello, ${name}!`;
}
}
}
const message = Utils.greet("Ada");
console.log(message);
This produces Cannot find name 'Utils', because Utils only exists as a property of App, not as its own name in scope. It’s a common mistake when refactoring flat namespaces into nested ones and forgetting to update call sites (or when you meant to use an import Alias = App.Utils but left it out).
The corrected version uses the fully qualified path:
namespace App {
export namespace Utils {
export function greet(name: string): string {
return `Hello, ${name}!`;
}
}
}
const message = App.Utils.greet("Ada");
console.log(message);
Output:
Hello, Ada!
Best Practices
- Prefer ES modules (
import/export) for new application code — reserve namespaces for global scripts, ambient declarations, and legacy code. - Never mix namespaces with a module-based build unnecessarily; if a file already has top-level
import/export, treat it as a module and avoid also relying on global namespace merging. - Keep namespace nesting shallow (one or two levels) — deeply nested namespaces make call sites verbose and hard to read.
- Use
import Alias = A.B.Cto shorten long, frequently used namespace paths within a file. - Only export what consumers actually need; keep helper functions and internal state unexported to preserve encapsulation.
- When you do use multi-file namespaces, use triple-slash
/// <reference path="..." />directives so the compiler understands the file ordering and can merge declarations correctly. - Remember that namespace members are real runtime properties on an object, not erased like pure type declarations — there’s a small but nonzero runtime cost compared to ES module imports that a bundler can tree-shake.
Practice Exercises
- Create a namespace called
Geometrywith an exportedCircleinterface (aradius: numberfield) and an exported functionarea(c: Circle): numberthat returnsMath.PI * c.radius * c.radius. Log the area of a circle with radius 3. - Build a nested namespace
Store.Inventorycontaining an exported classProductwithnameandpricefields and a methoddescribe()that returns a formatted string. Instantiate it from outside using both the full path and animport Alias = Store.Inventoryshorthand, and confirm both produce the same output. - Declare a namespace
Loggertwice in the same file (declaration merging) — once exporting aninfo(message: string): voidfunction, once exporting awarn(message: string): voidfunction — then call both from a single combinedLoggerreference.
Summary
- A
namespacegroups related interfaces, functions, classes, and variables under one name, exposing only members markedexport. - Namespaces compile to a plain JavaScript object built with an IIFE; all type information (interfaces, type annotations) is erased, but runtime members remain as real properties.
- Declaring the same namespace name more than once causes declaration merging — all declarations combine into a single namespace object, which is how multi-file namespaces were historically organized.
- Nested namespaces are accessed with a full dotted path (
Outer.Inner.member);import Alias = A.Bcreates a shorthand for long paths. - Modern TypeScript recommends ES modules for application code; namespaces are best reserved for non-bundled scripts, ambient declaration files, and legacy codebases.
