javascriptroom guide

Understanding TypeScript Modules and Namespaces

Without structure, code lives in the global scope, leading to naming collisions (e.g., two functions named `calculate()`), unclear dependencies, and difficulty reusing code. Modules and namespaces solve these problems by: - **Encapsulating code**: Restricting access to internal members by default. - **Grouping related code**: Organizing functions, classes, and variables logically. - **Enabling reusability**: Allowing code to be shared across files or projects. TypeScript builds on JavaScript’s module system and adds namespaces (originally called “internal modules”) to further enhance organization. Let’s start with modules.

In the world of programming, organizing code is as critical as writing it. As applications grow, unstructured code becomes难以维护, prone to conflicts, and hard to scale. TypeScript, a superset of JavaScript, offers powerful tools to address these challenges: modules and namespaces.

In this blog, we’ll dive deep into TypeScript modules and namespaces, exploring their purpose, syntax, use cases, and key differences. By the end, you’ll know when to use each and how to leverage them to write clean, scalable code.

Table of Contents

  1. Introduction to Code Organization
  2. What Are TypeScript Modules?
  3. What Are TypeScript Namespaces?
  4. Modules vs. Namespaces: Key Differences
  5. When to Use Modules vs. Namespaces
  6. Advanced Topics
  7. Conclusion
  8. References

What Are TypeScript Modules?

A module is a self-contained unit of code in a file. By default, all variables, functions, and classes declared in a module are scoped to that module (not global). To share them, you explicitly export them, and other modules import them.

TypeScript modules are file-based: Each .ts file is a module, and its filename often reflects its purpose (e.g., math.ts, userService.ts).

Types of Modules

TypeScript supports two primary module systems:

1. ES Modules (ESM)

The modern standard, used in browsers and Node.js (v14+). It uses import/export syntax.

2. CommonJS

Traditional module system used in Node.js (before ESM). It uses require() and module.exports.

TypeScript compiles modules to either format via the module setting in tsconfig.json (e.g., module: "ESNext" for ESM, module: "CommonJS" for Node.js).

Exporting from Modules

To make module members available to other modules, use the export keyword. There are two main export types:

Named Exports

Export multiple members individually. Use curly braces {} when importing.

Example: math.ts

// Named exports
export const PI = 3.14159;

export function add(a: number, b: number): number {
  return a + b;
}

export class Calculator {
  multiply(a: number, b: number): number {
    return a * b;
  }
}

Default Export

Export a single “primary” member per module. Omit curly braces when importing.

Example: logger.ts

// Default export (only one per module)
export default function log(message: string): void {
  console.log(`[LOG]: ${message}`);
}

You can mix named and default exports:

// src/utils.ts
export const version = "1.0.0"; // Named export
export default function greet(name: string): string { // Default export
  return `Hello, ${name}!`;
}

Importing into Modules

Use import to access exported members from other modules.

Importing Named Exports

Use curly braces to import specific named members. You can alias members with as to avoid conflicts.

Example: app.ts

// Import named exports from math.ts
import { PI, add, Calculator } from "./math"; 
// "./math" is a relative path (adjust based on file location)

console.log(PI); // 3.14159
console.log(add(2, 3)); // 5
const calc = new Calculator();
console.log(calc.multiply(4, 5)); // 20

// Alias a named export
import { add as sum } from "./math";
console.log(sum(10, 20)); // 30

Importing Default Exports

Omit curly braces for default exports. You can name the imported member anything.

Example: app.ts

// Import default export from logger.ts
import log from "./logger"; 
log("TypeScript modules are cool!"); // [LOG]: TypeScript modules are cool!

Importing the Entire Module as a Namespace

Import all exports into a single namespace object:

import * as MathUtils from "./math";
console.log(MathUtils.PI); // 3.14159
console.log(MathUtils.add(2, 3)); // 5

Importing for Side Effects

Import a module solely for its side effects (e.g., initializing a library, adding polyfills):

// Runs the code in "./analytics.ts" but doesn't import any members
import "./analytics"; 

Module Resolution

Module resolution is the process by which TypeScript finds the file corresponding to an import statement. It supports two strategies (configured via moduleResolution in tsconfig.json):

1. Relative Paths

Import modules in the same project using paths like ./math (current directory) or ../utils/logger (parent directory).

Example:

import { add } from "./math"; // Imports ./math.ts

2. Non-Relative Paths

Import modules from node_modules or custom directories (configured via baseUrl and paths in tsconfig.json).

Example: tsconfig.json

{
  "compilerOptions": {
    "baseUrl": "./src", // Root directory for non-relative imports
    "paths": {
      "@utils/*": ["utils/*"] // Alias: @utils/logger → src/utils/logger.ts
    }
  }
}

Now you can import:

import { greet } from "@utils/greet"; // Resolves to src/utils/greet.ts

Common Resolution Issues:

  • “Module not found”: Check file paths, baseUrl, and paths in tsconfig.json.
  • Missing .d.ts files: For third-party libraries without TypeScript support, install types (e.g., npm install @types/lodash).

What Are TypeScript Namespaces?

Namespaces (originally “internal modules”) group related code within a single file to avoid global scope pollution. They are ideal for organizing code in small applications or grouping utility functions in one file.

Unlike modules, namespaces are not file-based—you can have multiple namespaces in a single file, and they can span multiple files (with extra setup).

Namespace Syntax

Define a namespace with the namespace keyword. Members are scoped to the namespace and must be explicitly exported to be accessible outside.

Example: shapes.ts

namespace Shapes {
  // Internal member (not exported, only accessible within Shapes)
  const DEFAULT_COLOR = "black";

  // Exported member (accessible outside the namespace)
  export class Circle {
    radius: number;

    constructor(radius: number) {
      this.radius = radius;
    }

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

  // Exported nested namespace
  export namespace SquareUtils {
    export function perimeter(sideLength: number): number {
      return 4 * sideLength;
    }
  }
}

Using Namespace Members

To use exported members, reference the namespace with dot notation:

// Use Shapes.Circle
const circle = new Shapes.Circle(5);
console.log(circle.area()); // ~78.54

// Use nested namespace
console.log(Shapes.SquareUtils.perimeter(4)); // 16

Using Namespaces Across Files

Namespaces can span multiple files using triple-slash directives (see Advanced Topics). For example:

File 1: math-utils.ts

namespace MathUtils {
  export function add(a: number, b: number): number {
    return a + b;
  }
}

File 2: math-utils-extended.ts

/// <reference path="math-utils.ts" /> // Include the first namespace
namespace MathUtils {
  // Merge with the existing MathUtils namespace
  export function subtract(a: number, b: number): number {
    return a - b;
  }
}

File 3: app.ts

/// <reference path="math-utils-extended.ts" />
console.log(MathUtils.add(5, 3)); // 8
console.log(MathUtils.subtract(5, 3)); // 2

Modules vs. Namespaces

FeatureModulesNamespaces
ScopeFile-scoped (each file is a module).Scoped to the namespace block.
File BoundaryOne module per file.Multiple namespaces per file.
Sharing CodeUse import/export across files.Use triple-slash directives or merge namespaces.
ReusabilityIdeal for large apps (code splitting, dependencies).Best for small apps or single-file grouping.
Modern RecommendationPreferred for most projects.Legacy; use modules instead.

When to Use Which?

  • Use Modules When:

    • Building large applications (code splitting, lazy loading).
    • Sharing code across files or projects.
    • Using modern tooling (Webpack, Vite, Rollup).
    • Targeting ES modules or CommonJS.
  • Use Namespaces When:

    • Organizing small amounts of code in a single file.
    • Avoiding global scope pollution in simple scripts.
    • Working with legacy code that uses namespaces.

Advanced Topics

Merging Namespaces

TypeScript allows merging two namespaces with the same name, combining their members. This is useful for extending existing namespaces.

Example:

// First namespace
namespace Utils {
  export function log(message: string): void {
    console.log(message);
  }
}

// Merge with a second namespace of the same name
namespace Utils {
  export function warn(message: string): void {
    console.warn(message);
  }
}

// Now Utils has both log() and warn()
Utils.log("Hello"); // "Hello"
Utils.warn("Danger!"); // "Danger!"

Triple-Slash Directives

Triple-slash directives (/// <reference ... />) tell TypeScript to include another file during compilation. They are commonly used with namespaces to link files.

Syntax:

/// <reference path="path/to/other-file.ts" />

This ensures the referenced file is loaded before the current file, making its namespaces available.

Conclusion

Modules and namespaces are powerful tools for organizing TypeScript code:

  • Modules are file-based, use import/export, and are ideal for large, scalable applications.
  • Namespaces group code within files, use namespace syntax, and are best for small projects or legacy code.

Modern TypeScript development favors modules, but namespaces still have niche use cases. By choosing the right tool for the job, you’ll write cleaner, more maintainable code.

References