Module Systems & Namespaces

TypeScript Intermediate
Course 2 Β· Chapter 5 Β· Module Systems & Namespaces

πŸ“¦ Module Systems & Namespaces

As codebases grow, organization matters. This chapter covers ES modules (the modern standard), CommonJS (Node.js legacy), path mapping (clean imports), barrel exports (organized re-exports), and namespaces (TypeScript-specific grouping). You'll learn how to structure code across files without chaos.

🌍 ES Modules: The Standard

ES modules (ESM) are the modern standard. Files are modules; imports/exports are explicit:

// math.ts
export function add(a: number, b: number) {
  return a + b;
}

export const PI = 3.14159;

// app.ts
import { add, PI } from "./math";  // .ts extension is optional

console.log(add(2, 3));     // 5
console.log(PI);              // 3.14159

Named Exports

export function foo() β€” export by name. Import with { foo }.

Default Export

export default class User β€” one default per file. Import as import User from.

Re-export

export { foo } from "./module" β€” pass-through import/export (barrel exports).

Namespace Import

import * as math from "./math" β€” import all as math.add().

πŸ“Ό CommonJS: Node.js Legacy

CommonJS (CJS) is Node.js's original module system. Still widely used, but ES modules are preferred:

// math.ts (CommonJS style)
function add(a: number, b: number) {
  return a + b;
}

module.exports = { add };

// app.ts (CommonJS style)
const { add } = require("./math");
console.log(add(2, 3));

ES Modules vs CommonJS

CommonJS (require/module.exports)
const math = require("./math");
module.exports = { myFunc };
  • Dynamic (runtime)
  • Synchronous
  • Node.js built-in
ES Modules (import/export)
import math from "./math";
export { myFunc };
  • Static (compile-time)
  • Asynchronous
  • JavaScript standard
Modern projects: Use ES modules (set "module": "ESNext" in tsconfig.json). CommonJS is legacy but still needed for older Node.js versions.

πŸ—ΊοΈ Path Mapping: Cleaner Imports

Deep nested paths are messy. Path mapping lets you create short aliases:

// tsconfig.json
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"],
      "@utils/*": ["src/utils/*"],
      "@components/*": ["src/components/*"]
    }
  }
}

// Instead of:
import { formatDate } from "../../../../utils/format";

// Write:
import { formatDate } from "@utils/format";
πŸ’‘ Path Alias Conventions

@ prefix is common in modern projects (similar to npm scopes). Use @utils, @types, @components for clarity. Keep the mapping shallow so paths stay readable.

πŸ“¦ Barrel Exports: Organized Re-exports

A barrel export (index.ts) re-exports from submodules for cleaner public APIs:

// src/utils/string.ts
export function capitalize(s: string) {
  return s.charAt(0).toUpperCase() + s.slice(1);
}

// src/utils/math.ts
export function sum(a: number, b: number) {
  return a + b;
}

// src/utils/index.ts (barrel export)
export * from "./string";
export * from "./math";

// app.ts β€” clean, single import
import { capitalize, sum } from "@utils";

Without barrel: You'd need two imports. With barrel: One import gets everything.

πŸ“‹ Ambient Declarations: Types for External Code

Sometimes you use libraries without type definitions. Ambient declarations (`.d.ts` files) provide types:

// types/global.d.ts
// Declare a global variable
declare const DEBUG: boolean;

// Declare a module (for libraries without types)
declare module "my-untyped-library" {
  export function doSomething(x: any): string;
}

// In app.ts
console.log(DEBUG);  // βœ… TypeScript knows about it

import { doSomething } from "my-untyped-library";
const result = doSomething(42);  // βœ… Typed

πŸ”· TypeScript Namespaces (Legacy Grouping)

Namespaces group related types without ES modules. They're pre-module, but still used in legacy codebases:

// math.ts
namespace Math {
  export function add(a: number, b: number) {
    return a + b;
  }

  export const PI = 3.14159;
}

// app.ts
const result = Math.add(2, 3);  // Access via namespace
Avoid namespaces in new code. Use ES modules instead. Namespaces are kept for backward compatibility but modules are cleaner and standard.

πŸ—οΈ Real-World Pattern: Well-Organized Project

Clean Project Structure with Path Aliases & Barrels

// Directory structure:
// src/
//   β”œβ”€β”€ types/          ← TypeScript types
//   β”‚   β”œβ”€β”€ index.ts    (barrel)
//   β”‚   └── user.ts
//   β”œβ”€β”€ utils/          ← Utility functions
//   β”‚   β”œβ”€β”€ index.ts    (barrel)
//   β”‚   β”œβ”€β”€ format.ts
//   β”‚   └── validate.ts
//   β”œβ”€β”€ services/       ← Business logic
//   β”‚   β”œβ”€β”€ index.ts    (barrel)
//   β”‚   └── api.ts
//   └── app.ts          ← Entry point

// tsconfig.json
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@types/*": ["src/types/*"],
      "@utils/*": ["src/utils/*"],
      "@services/*": ["src/services/*"]
    }
  }
}

// src/types/index.ts
export * from "./user";

// src/utils/index.ts
export * from "./format";
export * from "./validate";

// src/app.ts β€” clean imports
import { User } from "@types";
import { formatDate, validateEmail } from "@utils";
import { UserService } from "@services";

const user: User = { id: 1, email: "alice@example.com" };
console.log(formatDate(new Date()));

πŸ’» Coding Challenges

Challenge 1: Create a Barrel Export

Create three utility modules (math.ts, string.ts, array.ts) with exported functions. Then create an index.ts barrel that re-exports all. Test importing from the barrel.

Goal: Practice barrel exports and namespace organization.

β†’ Solution

Challenge 2: Set Up Path Aliases

Create a tsconfig.json with path aliases (@utils, @types, @services). Set up dummy modules under each and import using the aliases.

Goal: Configure and use path mapping.

β†’ Solution

Challenge 3: Ambient Declaration

Create a .d.ts file that declares a global DEBUG constant and a module type for an external library. Write code that uses both.

Goal: Practice ambient declarations for untyped external code.

β†’ Solution

⚠️ Gotcha: Circular Dependencies

Be careful with circular imports: A imports B, B imports A. TypeScript won't error, but at runtime it can cause undefined values. Refactor to break the cycleβ€”move shared types to a third file both can import from.

🎯 What's Next

With module systems mastered, we'll explore Advanced OOP β€” abstract classes, access modifiers, static members, and design patterns in TypeScript.