1. Introduction
Namespaces (formerly called "internal modules") are a TypeScript-specific construct for grouping related code — variables, functions, classes, interfaces — under a single named container, using the namespace keyword. They predate ES modules in the TypeScript language and were originally the primary way to organize code before import/export syntax became standard in JavaScript.
Cricket analogy: A namespace like namespace Scoring { ... } is like grouping every scoring function (runs, extras, wickets) under one team dressing-room banner before the ICC's modern import/export transfer rules existed, so everything lived under one shared name.
Namespaces compile to plain JavaScript objects that attach properties to a shared global (or module-scoped) variable, which makes them useful in contexts without a module loader — for example, multiple <script> tags sharing global state in a browser, or organizing large ambient type declaration files. In modern application code, ES modules are strongly preferred, and namespaces are considered legacy, but they still appear in older codebases and in certain .d.ts declaration file patterns.
Cricket analogy: A namespace compiling to a plain object attaching properties to one shared global variable is like several broadcast vans on the same ground sharing one shared scoreboard feed without a formal relay system — useful for old stadium setups, but modern grounds use dedicated per-camera feeds (ES modules) instead.
2. Syntax
// shapes.ts
namespace Shapes {
export interface Shape {
area(): number;
}
export class Circle implements Shape {
constructor(private radius: number) {}
area(): number {
return Math.PI * this.radius ** 2;
}
}
// Not exported — private to the namespace
function describe(shape: Shape): string {
return `Area: ${shape.area().toFixed(2)}`;
}
export function report(shape: Shape): string {
return describe(shape);
}
}
// usage.ts (same compiled output / with <reference> in older setups)
const c = new Shapes.Circle(2);
console.log(Shapes.report(c));
// Nested namespace
namespace Company.Department {
export const name = "Engineering";
}
console.log(Company.Department.name);3. Explanation
ES modules (import/export) are the modern standard, while namespace (formerly "internal modules") is largely discouraged for new code but still seen in older codebases and global-script contexts. A namespace block compiles to an immediately-invoked function expression (IIFE) that builds up a plain object; only members marked export are attached to that object and visible from outside, exactly mirroring how ES module exports work, but scoped to a single global identifier instead of a file-based module graph.
Cricket analogy: A namespace block compiling to an IIFE is like an old-school scoring booth that builds one shared results object behind closed doors, only posting the marked export stats onto the public board, exactly mirroring how a modern module publishes its exports — except everything hangs off one shared board instead of separate per-match sheets.
Namespaces can be split across multiple files using triple-slash reference directives (/// <reference path="..." />) so the compiler knows how to stitch them together and preserve declaration order, and they can be nested (namespace A.B { ... }) to mimic package-like hierarchies. Because everything ultimately hangs off one runtime object, namespaces don't get the same tree-shaking, lazy-loading, or per-file dependency resolution benefits that bundlers give to ES modules.
Cricket analogy: Splitting namespace Cricket.Scoring across files with /// <reference path="..." /> is like stitching together multiple scorers' handwritten sheets in a fixed order into one master scorebook; because it's all one shared book, a bundler can't tree-shake out the unused fielding-drill notes the way it could drop an unused ES module file.
Namespaces remain genuinely useful in one place: ambient declaration files (.d.ts) for libraries that expose a single global variable (e.g., a script-tag library like an old jQuery plugin), where there is no module system to import from and the library's shape must be described as a namespace on the global scope.
Gotcha: mixing namespaces and ES modules in the same file is legal TypeScript but produces confusing, hard-to-maintain code, and combining namespace with export = / import ... = require(...) (needed to actually export a namespace from a module) is a common source of interop errors with bundlers that expect standard ESM default/named exports. For new projects, prefer ES modules entirely and avoid namespaces unless you're maintaining legacy code or writing global-script declaration files.
4. Example
// validation.ts — a namespace split conceptually into two logical groups
namespace Validation {
export interface StringValidator {
isValid(s: string): boolean;
}
export class LettersOnlyValidator implements StringValidator {
isValid(s: string): boolean {
return /^[A-Za-z]+$/.test(s);
}
}
export class ZipCodeValidator implements StringValidator {
isValid(s: string): boolean {
return /^\d{5}(-\d{4})?$/.test(s);
}
}
}
const validators: { [name: string]: Validation.StringValidator } = {
letters: new Validation.LettersOnlyValidator(),
zip: new Validation.ZipCodeValidator(),
};
for (const name of Object.keys(validators)) {
console.log(`${name}: ${validators[name].isValid("12345")}`);
}5. Output
letters: false
zip: true
Compiled JavaScript wraps the namespace body in an IIFE roughly equivalent to:
var Validation;
(function (Validation) {
... class definitions attached as Validation.LettersOnlyValidator, Validation.ZipCodeValidator ...
})(Validation || (Validation = {}));
Only the `export`-ed classes/interfaces are accessible as Validation.X from outside the IIFE.6. Key Takeaways
- Namespaces (formerly "internal modules") group related code under one named object using the
namespacekeyword. - Only members marked
exportinside a namespace are visible outside it; the rest stay private to the compiled IIFE. - Namespaces compile to a runtime object built by an IIFE, not to a file-based module.
- ES modules are the modern standard for application code; namespaces are legacy, mainly useful for global-script
.d.tsdeclarations. - Namespaces can be nested (
namespace A.B) and split across files with triple-slash references, but lose bundler-level tree-shaking benefits.
Practice what you learned
1. What was the earlier name for TypeScript namespaces before ES modules became the JavaScript standard?
2. In modern TypeScript projects, what is the recommended approach for organizing application code across files?
3. What does a `namespace` block compile down to at runtime?
4. Where do namespaces still have a genuinely useful role in modern TypeScript?
5. What is a common source of interop errors when a namespace needs to be exported from a module?
Was this page helpful?
You May Also Like
Modules in TypeScript
How TypeScript uses ES module syntax (import/export) to organize code across files, with type-only imports and module resolution basics.
Declaration Files (.d.ts) in TypeScript
How .d.ts files describe the shape of JavaScript code for the TypeScript compiler, including DefinitelyTyped (@types) packages.
Interfaces in TypeScript
Learn how TypeScript interfaces describe the shape of objects using structural typing, and why they are the primary tool for contract-based design.
Classes in TypeScript
Learn how TypeScript classes add type-checked properties, methods, and constructor parameter property shorthand on top of ES2015 classes.
Related Reading
Related Study Notes in Programming
Browse all study notesApache Spark Study Notes
Programming · 30 topics
ProgrammingApache Flink Study Notes
Programming · 30 topics
ProgrammingHadoop Study Notes
Programming · 30 topics
ProgrammingSnowflake Study Notes
Programming · 30 topics
ProgrammingApache Airflow Study Notes
Programming · 30 topics
Programmingdbt (Data Build Tool) Study Notes
Programming · 30 topics