TypeScript Namespaces
A namespace is a named container for related code. It groups types, functions and classes under one name so they do not clash with other code.
Understanding TypeScript Namespaces
TypeScript namespaces (once called "internal modules") give you a way to organize code and avoid naming conflicts by putting related functionality inside a named container.
They help structure large codebases and keep scope under control in a clean, maintainable way.
Key concepts:
- Logical grouping: organize related code into named containers
- Scope management: control which parts of the code are visible from outside
- Name collision prevention: avoid conflicts between things with similar names
- Code organization: structure large applications in a hierarchy
When to use namespaces:
- Organizing code in large legacy applications
- Working with global libraries
- Migrating from older JavaScript codebases
- Working with code that must be available globally
Note: Namespaces are still fully supported, but modern applications usually use ES modules (import/export) instead, because modules give better modularity and let bundlers drop unused code (tree-shaking). Understanding namespaces is still valuable for maintaining legacy codebases and for some library development scenarios.
Basic Namespace Syntax
A namespace is defined with the namespace keyword followed by a block. Only members marked export can be used from outside the block; everything else stays private to the namespace.
Example
The Validation namespace exports an interface and two classes, while lettersRegexp is private to it. Outside, the classes are reached with dot notation: Validation.LettersValidator.
namespace syntax with exported and private members.
Declare a Validation namespace that exports a StringValidator interface (isValid(s: string): boolean), keeps a non-exported lettersRegexp (/^[A-Za-z]+$/), and exports two classes implementing the interface: LettersValidator (letters only) and ZipCodeValidator (digits only and exactly five characters). Outside the namespace create one of each via Validation.<Class> and log isValid for "Hello", "Hello123", "12345" and "1234".
Output: TypeScript file and captured output
namespace Validation {
// Everything inside this block belongs to the Validation namespace
// Export things you want to make available outside the namespace
export interface StringValidator {
isValid(s: string): boolean;
}
// This is private to the namespace (not exported)
const lettersRegexp = /^[A-Za-z]+$/;
// Exported class - available outside the namespace
export class LettersValidator implements StringValidator {
isValid(s: string): boolean {
return lettersRegexp.test(s);
}
}
// Another exported class
export class ZipCodeValidator implements StringValidator {
isValid(s: string): boolean {
return /^[0-9]+$/.test(s) && s.length === 5;
}
}
}
// Using the namespace members
let letterValidator = new Validation.LettersValidator();
let zipCodeValidator = new Validation.ZipCodeValidator();
console.log(letterValidator.isValid("Hello")); // true
console.log(letterValidator.isValid("Hello123")); // false
console.log(zipCodeValidator.isValid("12345")); // true
console.log(zipCodeValidator.isValid("1234")); // false - wrong length
Advanced Namespace Features
Namespaces can be nested inside each other to build a hierarchy, and long namespace paths can be shortened with aliases.
Nested Namespaces
App contains two exported inner namespaces, Utils and Models. Members are reached through the full path, such as App.Utils.log; App.log would be an error because log is not a direct member of App.
App namespace containing an exported Utils namespace (with log and error functions that print [LOG]: msg and [ERROR]: msg) and an exported Models namespace (a User interface with id, name, email, and a UserService class whose getUser(id) returns John Doe / [email protected]). Call App.Utils.log("Application starting"), load user 1 through App.Models.UserService, log User loaded: <name>, and leave a commented-out App.log(...) call noting it would be an error.
Output: TypeScript file and captured output
namespace App {
export namespace Utils {
export function log(msg: string): void {
console.log(`[LOG]: ${msg}`);
}
export function error(msg: string): void {
console.error(`[ERROR]: ${msg}`);
}
}
export namespace Models {
export interface User {
id: number;
name: string;
email: string;
}
export class UserService {
getUser(id: number): User {
return { id, name: "John Doe", email: "[email protected]" };
}
}
}
}
// Using nested namespaces
App.Utils.log("Application starting");
const userService = new App.Models.UserService();
const user = userService.getUser(1);
App.Utils.log(`User loaded: ${user.name}`);
// This would be a type error in TypeScript
// App.log("directly accessing log"); // Error - log is not a direct member of App
Namespace Aliases
You can create an alias for a namespace or one of its members with import Alias = Some.Long.Path. This is not a module import — it just gives a long name a short one. Components aliases the innermost namespace and Button aliases a single class.
import Alias = Some.Long.Path.
Declare VeryLongNamespace.DeeplyNested.Components (three nested namespaces) exporting Button and TextField classes whose display() methods log Button displayed and TextField displayed. Create and display a button three ways: through the full path, through a Components alias of the innermost namespace, and through a Button alias of the class itself.
Output: TypeScript file and captured output
namespace VeryLongNamespace {
export namespace DeeplyNested {
export namespace Components {
export class Button {
display(): void {
console.log("Button displayed");
}
}
export class TextField {
display(): void {
console.log("TextField displayed");
}
}
}
}
}
// Without alias - very verbose
const button1 = new VeryLongNamespace.DeeplyNested.Components.Button();
button1.display();
// With namespace alias
import Components = VeryLongNamespace.DeeplyNested.Components;
const button2 = new Components.Button();
button2.display();
// With specific member alias
import Button = VeryLongNamespace.DeeplyNested.Components.Button;
const button3 = new Button();
button3.display();
Working with Multi-file Namespaces
Large applications often split code across several files. A TypeScript namespace can be spread over multiple files and combined at compile time using reference comments.
A reference comment such as /// <reference path="validators.ts" /> tells TypeScript that one file depends on another, so it knows the order in which to combine them. The four files below together form one Validation namespace.
To compile these files into a single JavaScript file, use tsc --outFile sample.js main.ts.
validators.ts
The first file declares the Validation namespace and exports the StringValidator interface.
namespace Validation {
export interface StringValidator {
isValid(s: string): boolean;
}
}
letters-validator.ts
This file references validators.ts and extends the same Validation namespace with a LettersValidator class.
/// <reference path="validators.ts" />
namespace Validation {
const lettersRegexp = /^[A-Za-z]+$/;
export class LettersValidator implements StringValidator {
isValid(s: string): boolean {
return lettersRegexp.test(s);
}
}
}
zipcode-validator.ts
This file also references validators.ts and adds a ZipCodeValidator class to the namespace.
/// <reference path="validators.ts" />
namespace Validation {
const zipCodeRegexp = /^[0-9]+$/;
export class ZipCodeValidator implements StringValidator {
isValid(s: string): boolean {
return zipCodeRegexp.test(s) && s.length === 5;
}
}
}
main.ts
The main file references all three files, creates both validators, and prints whether each sample string matches each validator.
/// <reference path="validators.ts" />
/// <reference path="letters-validator.ts" />
/// <reference path="zipcode-validator.ts" />
// Now you can use the validators from multiple files
let validators: { [s: string]: Validation.StringValidator } = {};
validators["letters"] = new Validation.LettersValidator();
validators["zipcode"] = new Validation.ZipCodeValidator();
// Some samples to validate
let strings = ["Hello", "98052", "101"];
// Validate each
strings.forEach(s => {
for (let name in validators) {
console.log(`"${s}" - ${validators[name].isValid(s) ? "matches" : "does not match"} ${name}`);
}
});
Namespaces vs. Modules
Knowing when to use namespaces and when to use ES modules is important:
- Modules are the preferred way to organize code in modern TypeScript applications
- Namespaces are still useful in specific cases, such as declaration merging or working with legacy code
- Modules have better tooling support and tree-shaking
- Namespaces can be handy for creating global libraries
Here is a feature-by-feature comparison of namespaces and ES modules (import/export):
- Recommended scope/scale — Namespaces: simpler setups, small apps, legacy codebases. Modules: modern apps of any size; preferred for new projects.
- Syntax and usage — Namespaces: global access via dot notation (for example
MyNS.Member). Modules: explicitimport/exportwith file paths. - Loading/bundling — Namespaces: no loader required; can emit a single file with
--outFile. Modules: usually go through a bundler or loader (Vite, webpack, etc.). - Splitting across files — Namespaces: possible with
/// <reference />comments. Modules: natural; each file is a module with explicit exports and imports. - Tree-shaking — Namespaces: limited; bundlers find it hard to drop unused code. Modules: excellent; designed for dead-code elimination.
- Global scope — Namespaces: encourage (namespaced) globals. Modules: avoid globals; dependencies are explicit.
- Augmentation/merging — Namespaces: strong support through declaration merging. Modules: module augmentation is possible but more constrained.
- Tooling and ecosystem — Namespaces: work, but are less aligned with modern tooling. Modules: best support across modern tooling and platforms.
- Best used for — Namespaces: legacy libraries, global scripts, ambient types. Modules: all new development, libraries and applications.
Advanced Namespace Patterns
Two more patterns are common with namespaces: augmenting an existing namespace through declaration merging, and putting generic types and classes inside a namespace.
Augmenting a namespace
A second declare namespace Express block merges with the first, adding requestTime and log to Request and a new UserSession interface; the usage code assumes an express() function from a library.
// Original namespace
declare namespace Express {
interface Request {
user?: { id: number; name: string };
}
interface Response {
json(data: any): void;
}
}
// Later in your application (e.g., in a .d.ts file)
declare namespace Express {
// Augment the Request interface
interface Request {
// Add custom properties
requestTime?: number;
// Add methods
log(message: string): void;
}
// Add new types
interface UserSession {
userId: number;
expires: Date;
}
}
// Usage in your application
const app = express();
app.use((req: Express.Request, res: Express.Response, next) => {
// Augmented properties and methods are available
req.requestTime = Date.now();
req.log('Request started');
next();
});
Namespaces with Generics
The DataStorage namespace exports a generic Repository<T> interface and an InMemoryRepository<T> class that implements it, used here with a User type.
// Generic namespace example
namespace DataStorage {
export interface Repository<T> {
getAll(): T[];
getById(id: number): T | undefined;
add(item: T): void;
update(id: number, item: T): boolean;
delete(id: number): boolean;
}
// Concrete implementation
export class InMemoryRepository<T> implements Repository<T> {
private items: T[] = [];
getAll(): T[] {
return [...this.items];
}
getById(id: number): T | undefined {
return this.items[id];
}
add(item: T): void {
this.items.push(item);
}
update(id: number, item: T): boolean {
if (id >= 0 && id < this.items.length) {
this.items[id] = item;
return true;
}
return false;
}
delete(id: number): boolean {
if (id >= 0 && id < this.items.length) {
this.items.splice(id, 1);
return true;
}
return false;
}
}
}
// Usage
interface User {
id: number;
name: string;
email: string;
}
const userRepo = new DataStorage.InMemoryRepository<User>();
userRepo.add({ id: 1, name: 'John Doe', email: '[email protected]' });
const allUsers = userRepo.getAll();
Best Practices
Do:
- Use meaningful, hierarchical namespace names
- Export only what is needed from a namespace
- Use
/// <reference />comments to order files in multi-file namespaces - Consider modules for new projects
- Use
const enuminside namespaces for better performance - Document your namespaces with JSDoc comments
Don't:
- Create very deep namespace hierarchies (more than 2–3 levels)
- Pollute the global scope unnecessarily
- Mix namespaces and modules in one project without a clear strategy
- Use namespaces for small applications — prefer modules
Performance considerations:
- Large namespaces can increase bundle size
- Consider code splitting for large applications
- Watch out for circular dependencies in complex namespace structures
- Use
const enumfor better performance with constant values
Migrating from Namespaces to Modules
Moving from namespaces to ES modules usually means turning each namespace into its own file with exported members, and replacing dotted access with import statements.
Migration steps:
- Convert each namespace to a module file
- Replace namespace
exports with ES module exports - Update imports to use ES module syntax
- Configure your build system to handle modules
- Update tests to work with the new module structure
- Consider using a bundler like webpack or Rollup
- Update your
tsconfig.jsonto use"module": "ESNext"
Migration tools:
ts-migrate— an automated migration tool from Facebooktslintwith theno-namespacerule to detect namespaces- TypeScript's built-in refactoring tools
Before and after migration
The same UserService class first lives inside nested namespaces, then becomes an exported class in its own module file that is imported where needed.
// Before: Using namespaces
namespace MyApp {
export namespace Services {
export class UserService {
getUser(id: number) { /* ... */ }
}
}
}
// After: Using ES modules
// services/UserService.ts
export class UserService {
getUser(id: number) { /* ... */ }
}
// app.ts
import { UserService } from './services/UserService';
const userService = new UserService();