TypeScript Namespace

In TypeScript, a namespace is a way to group related code under a single name, providing a logical structure for your application. Namespaces help organize large codebases by avoiding "global scope pollution"—a situation where too many variables and functions sit in the global space, increasing the risk of name conflicts. They are particularly useful when working with older JavaScript code or libraries where modern ES modules might not be available.

 

What is a Namespace?

Think of a namespace as a container or a "named folder" for your code. In a large project, you might have two different functions named Validate(). Without a namespace, these would clash. By placing them in namespaces like UserValidation and ProductValidation, you can keep them separate and organized. Namespaces can encapsulate variables, functions, interfaces, and classes, keeping the internal implementation hidden while only exposing what is necessary.

Developer Tip: Namespaces were formerly known as "Internal Modules" in earlier versions of TypeScript. If you see old documentation referring to internal modules, they are talking about namespaces.

 

Creating a Namespace

To create a namespace in TypeScript, you use the namespace keyword, followed by the name of the namespace. Any code you want to be accessible from outside the namespace must be prefixed with the export keyword.

Example: Basic Namespace

namespace MathUtils {
  // This constant is private to the namespace
  const PI = 3.14;

  export function add(x: number, y: number): number {
    return x + y;
  }

  export function subtract(x: number, y: number): number {
    return x - y;
  }
}
  • In this example, we have created a namespace called MathUtils.
  • The export keyword makes add and subtract accessible to the rest of your application.
  • Notice that variables without export (like PI) are "private" and can only be used inside MathUtils.
Common Mistake: Forgetting to use the export keyword. If you don't export a function or class within a namespace, it will be invisible when you try to call it from outside, resulting in a "Property does not exist" error.

Example: Using a Namespace

let sum = MathUtils.add(10, 5);
console.log(sum); // Output: 15
  • To access members of a namespace, we use the "dot notation" (NamespaceName.MemberName).

 

Nested Namespaces

Namespaces can be nested within one another, allowing you to create a deep, hierarchical structure. This is great for very large libraries where you want to categorize code further (e.g., App.UI.Buttons).

Example: Nested Namespace

namespace Geometry {
  export namespace Shapes {
    export class Circle {
      constructor(public radius: number) {}

      area(): number {
        return Math.PI * this.radius ** 2;
      }
    }

    export class Square {
      constructor(public side: number) {}

      area(): number {
        return this.side ** 2;
      }
    }
  }
}
  • In this example, Shapes is a sub-category of Geometry. Both the inner namespace and the classes must be exported to be usable.
Watch Out: Deeply nesting namespaces can make your code verbose and harder to read. Try to keep your hierarchy shallow—usually one or two levels deep is plenty.

Example: Using Nested Namespace

let circle = new Geometry.Shapes.Circle(5);
console.log(circle.area()); // Output: 78.5398...

let square = new Geometry.Shapes.Square(4);
console.log(square.area()); // Output: 16

 

Using Interfaces with Namespaces

Namespaces aren't just for logic; they are excellent for grouping Types and Interfaces. This ensures that your data models don't conflict with models from other parts of the app.

Example: Interface inside a Namespace

namespace AppData {
  export interface User {
    id: number;
    name: string;
  }

  export class UserAccount implements User {
    constructor(public id: number, public name: string) {}
  }
}
  • The User interface is safely tucked away inside AppData. This allows you to have another User interface elsewhere (perhaps in a Database namespace) without a collision.

Example: Using Interface from a Namespace

let myUser: AppData.User = { id: 1, name: "Jane Doe" };
let account = new AppData.UserAccount(2, "John Smith");

 

Aliasing a Namespace

If you have deeply nested namespaces or long names, typing the full path every time becomes tedious. TypeScript allows you to create an alias using the import keyword.

Example: Alias Namespace

import GShapes = Geometry.Shapes;

let myCircle = new GShapes.Circle(10);
console.log(myCircle.area());
  • This is not a standard ES module import; it's a TypeScript-specific shortcut to simplify your code.

 

Namespaces vs Modules

This is the most important distinction to understand in modern TypeScript development.

  • Namespaces: These are a TypeScript-specific way to organize code. They are usually compiled into a single JavaScript file using the --outFile flag. They rely on the global scope to function.
  • Modules: (Recommended) Modules use import and export statements. They are the standard in modern JavaScript (ES6) and work perfectly with bundlers like Webpack or Vite.
Best Practice: Use Modules for modern web applications. Use Namespaces only if you are maintaining a legacy project or if you are writing a small script that will run in the browser without a module loader.

 

Summary

  • Namespaces provide a way to group related code and prevent global scope pollution.
  • Always use the export keyword to make namespace members available outside the block.
  • Nesting allows for complex organizational structures, but should be used sparingly.
  • Aliases help keep your code clean when dealing with long namespace paths.
  • While still supported, Modules are generally preferred over Namespaces for modern TypeScript development.

By using namespaces effectively, you can build modular, maintainable, and conflict-free TypeScript applications, especially when dealing with architectural patterns that don't rely on modern bundlers.