Skip to main content

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.

Prompt
Show the basic 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

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.

Prompt
Show nested namespaces accessed with dot notation. Declare an 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 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.

Prompt
Show namespace aliases created with 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

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.

script.ts
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.

script.ts
/// <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.

script.ts
/// <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.

script.ts
/// <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: explicit import/export with 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.

script.ts
// 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.

script.ts
// 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 enum inside 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 enum for 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:

  1. Convert each namespace to a module file
  2. Replace namespace exports with ES module exports
  3. Update imports to use ES module syntax
  4. Configure your build system to handle modules
  5. Update tests to work with the new module structure
  6. Consider using a bundler like webpack or Rollup
  7. Update your tsconfig.json to use "module": "ESNext"

Migration tools:

  • ts-migrate — an automated migration tool from Facebook
  • tslint with the no-namespace rule 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.

script.ts
// 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();