JavaScript and TypeScript course ยท Module 7: Advanced TypeScript

Namespaces, Declaration Merging, and Ambient Types

7 min read ยท
In this lesson5

"In Jurassic Park, we have dozens of systems - monitoring, genetics, security, logistics," explains Dennis Nedry, the park's lead programmer. "Each system has hundreds of variables, types, functions. Without proper organization, names would start repeating, and the chaos would become unmanageable. We need a way to separate each system while still allowing them to cooperate."

In TypeScript, there are several mechanisms for organizing code at the type level: namespaces, declaration merging, and ambient types. These techniques allow building well-organized, extensible type systems.

Namespaces

A namespace is a way of grouping related types, interfaces, functions, and variables under a common name. It prevents name collisions and logically organizes code.

// Namespace for the security system
namespace Security {
  export interface Camera {
    id: string;
    zone: string;
    isActive: boolean;
  }

  export interface Guard {
    id: string;
    name: string;
    assignedZone: string;
  }

  export type AlertLevel = "green" | "yellow" | "red" | "black";

  export function getAlertDescription(level: AlertLevel): string {
    switch (level) {
      case "green": return "All normal";
      case "yellow": return "Elevated vigilance";
      case "red": return "Threat - evacuate visitors";
      case "black": return "Critical - full evacuation";
    }
  }
}

// Namespace for the genetics system
namespace Genetics {
  export interface Sample {
    id: string;
    species: string;
    purity: number;
  }

  // This "Sample" does not collide with Security.Camera
  // because it's in a different namespace
  export function analyzeSample(sample: Sample): string {
    return `Sample analysis ${sample.id}: purity ${sample.purity}%`;
  }
}

// Usage - referencing through the namespace name
const camera: Security.Camera = { id: "CAM-01", zone: "T-Rex Zone", isActive: true };
const sample: Genetics.Sample = { id: "DNA-42", species: "Velociraptor", purity: 87 };

console.log(Security.getAlertDescription("red"));
console.log(Genetics.analyzeSample(sample));

Nested Namespaces

Namespaces can be nested, creating a hierarchical structure:

namespace JurassicPark {
  export namespace Monitoring {
    export interface SensorReading {
      sensorId: string;
      temperature: number;
      humidity: number;
      timestamp: Date;
    }

    export function isReadingNormal(reading: SensorReading): boolean {
      return reading.temperature >= 20 && reading.temperature <= 35;
    }
  }

  export namespace Feeding {
    export interface Schedule {
      dinosaurId: string;
      food: string;
      amountKg: number;
      time: string;
    }

    export function createSchedule(dinoId: string, food: string, kg: number, time: string): Schedule {
      return { dinosaurId: dinoId, food, amountKg: kg, time };
    }
  }
}

// Usage
const reading: JurassicPark.Monitoring.SensorReading = {
  sensorId: "TEMP-A3",
  temperature: 28,
  humidity: 65,
  timestamp: new Date()
};

const schedule = JurassicPark.Feeding.createSchedule("DINO-001", "Goat", 50, "08:00");
console.log("Temperature normal:", JurassicPark.Monitoring.isReadingNormal(reading));
console.log("Schedule:", JSON.stringify(schedule));

Declaration Merging

Declaration merging is a TypeScript mechanism where the compiler combines two or more separate declarations with the same name into a single definition. This is especially useful for extending existing types.

Interface Merging

The most common type of declaration merging is interface merging. When you declare the same interface multiple times, TypeScript merges them into one:

// Base dinosaur interface
interface ParkDinosaur {
  id: string;
  name: string;
  species: string;
}

// Extending the same interface - automatically merged
interface ParkDinosaur {
  weight: number;
  diet: "carnivore" | "herbivore" | "omnivore";
}

// Another extension
interface ParkDinosaur {
  enclosureId: string;
  dangerLevel: number;
}

// The resulting ParkDinosaur type has ALL fields from all declarations
const rex: ParkDinosaur = {
  id: "D-001",
  name: "Rexy",
  species: "Tyrannosaurus Rex",
  weight: 8000,
  diet: "carnivore",
  enclosureId: "ENC-A1",
  dangerLevel: 10
};

console.log(`${rex.name}: ${rex.weight}kg, danger: ${rex.dangerLevel}/10`);

Namespace Merging

Namespaces with the same name also merge:

namespace ParkSystems {
  export function getVersion(): string {
    return "1.0.0";
  }
}

namespace ParkSystems {
  export function getStatus(): string {
    return "Operational";
  }
}

// Both exports are available
console.log(ParkSystems.getVersion());
console.log(ParkSystems.getStatus());

Namespace Merging with a Class or Function

A namespace can extend a class with static properties:

class Enclosure {
  name: string;
  capacity: number;

  constructor(name: string, capacity: number) {
    this.name = name;
    this.capacity = capacity;
  }
}

// A namespace with the same name adds static members
namespace Enclosure {
  export interface Config {
    fenceVoltage: number;
    cameraCount: number;
    guardCount: number;
  }

  export const DEFAULT_CONFIG: Config = {
    fenceVoltage: 5000,
    cameraCount: 4,
    guardCount: 2
  };

  export function createHighSecurity(name: string, capacity: number): Enclosure {
    const enc = new Enclosure(name, capacity);
    // Use the default configuration...
    return enc;
  }
}

// Using the class and namespace together
const enc = new Enclosure("Raptor Zone", 6);
const config = Enclosure.DEFAULT_CONFIG;
const secureEnc = Enclosure.createHighSecurity("T-Rex Zone", 2);

console.log(`Enclosure: ${enc.name}, Default voltage: ${config.fenceVoltage}V`);

Ambient Declarations

The declare keyword tells TypeScript that a given variable, function, or class exists at runtime but is defined elsewhere (e.g., in a global script, external library). The compiler does not generate code for declare - it only checks types.

// Global variable declaration
declare const PARK_VERSION: string;
declare const MAX_DINOSAURS: number;

// Function declaration from an external library
declare function trackDinosaur(id: string): { x: number; y: number };
declare function sendAlert(message: string, zone: string): void;

// External module declaration
declare module "jurassic-utils" {
  export function calculateFeedingAmount(weight: number, species: string): number;
  export function getEnclosureRecommendation(dangerLevel: number): string;
}

// Class declaration from an external system
declare class ExternalTrackingSystem {
  connect(apiKey: string): Promise<void>;
  getPosition(dinoId: string): { lat: number; lng: number };
  disconnect(): void;
}

.d.ts Files - Type Declaration Files

Files with the .d.ts extension contain ONLY type declarations - no executable code. They serve to describe types for JavaScript libraries that don't have built-in types.

// Example jurassic-park.d.ts file
// (declarations only, no implementation)

declare namespace JurassicParkAPI {
  interface DinosaurResponse {
    id: string;
    name: string;
    species: string;
    location: { lat: number; lng: number };
  }

  interface EnclosureResponse {
    id: string;
    name: string;
    status: "normal" | "lockdown" | "breach";
    dinosaurIds: string[];
  }

  function fetchDinosaur(id: string): Promise<DinosaurResponse>;
  function fetchEnclosure(id: string): Promise<EnclosureResponse>;
  function getAllDinosaurs(): Promise<DinosaurResponse[]>;
}

Extending Existing Modules (Module Augmentation)

You can add types to existing modules using declare module:

// Adding fields to the Express Request interface (common case)
// File: types/express.d.ts
declare module "express" {
  interface Request {
    parkUser?: {
      id: string;
      role: "visitor" | "staff" | "admin";
      clearanceLevel: number;
    };
  }
}

// Extending global types
declare global {
  interface Array<T> {
    // Add a method to Array
    findDinosaur(predicate: (item: T) => boolean): T | undefined;
  }

  interface Window {
    parkConfig: {
      apiUrl: string;
      version: string;
      debugMode: boolean;
    };
  }
}

// Important: the file must have at least one import/export
// for TypeScript to treat it as a module
export {};

Practical Example - Organizing the Park System

Below is an example combining namespaces, declaration merging, and ambient declarations in one system:

// Namespace with base types
namespace Park {
  export interface Entity {
    id: string;
    createdAt: Date;
  }

  export type Priority = "low" | "medium" | "high" | "critical";
}

// Declaration merging - extending Park with new types
namespace Park {
  export interface Dinosaur extends Entity {
    name: string;
    species: string;
    dangerLevel: number;
  }

  export interface Enclosure extends Entity {
    name: string;
    capacity: number;
    residents: string[];
  }
}

// Another extension - helper functions
namespace Park {
  export function isDangerous(dino: Dinosaur): boolean {
    return dino.dangerLevel >= 7;
  }

  export function getEnclosureStatus(enc: Enclosure): string {
    const ratio = enc.residents.length / enc.capacity;
    if (ratio >= 1) return "FULL";
    if (ratio >= 0.75) return "Almost full";
    return "OK";
  }
}

// Usage - all extensions work together
const trex: Park.Dinosaur = {
  id: "D-001",
  createdAt: new Date(),
  name: "Rexy",
  species: "T-Rex",
  dangerLevel: 10
};

const enclosure: Park.Enclosure = {
  id: "E-001",
  createdAt: new Date(),
  name: "T-Rex Zone",
  capacity: 2,
  residents: ["D-001"]
};

console.log(`${trex.name} dangerous: ${Park.isDangerous(trex)}`);
console.log(`Enclosure ${enclosure.name}: ${Park.getEnclosureStatus(enclosure)}`);

Summary

Type organization mechanisms in TypeScript are powerful tools for building large, scalable systems:

  1. Namespaces - grouping related types and functions, avoiding name collisions
  2. Declaration merging - extending existing interfaces and namespaces with new fields and methods
  3. Ambient declarations (declare) - describing types for code defined elsewhere
  4. .d.ts files - files containing only type declarations (no code)
  5. Module augmentation - extending types in existing modules (e.g., Express, external libraries)

As Dennis Nedry says: "The system must be organized like dinosaur DNA - every gene in its place, but all cooperating in one organism. Namespaces are chromosomes, declaration merging is gene expression, and ambient types are information about the environment."

Code for this lesson: index.ts
// Namespace, declaration merging, ambient types - Jurassic Park
console.log("=== NAMESPACES AND DECLARATION MERGING ===\n");

// ===========================================
// 1. Namespace - organizing code
// ===========================================

namespace Security {
  export interface Camera {
    id: string;
    zone: string;
    isActive: boolean;
  }

  export type AlertLevel = "green" | "yellow" | "red";

  export function describe(level: AlertLevel): string {
    switch (level) {
      case "green": return "All OK";
      case "yellow": return "Heightened vigilance";
      case "red": return "ALARM!";
    }
  }
}

namespace Genetics {
  export interface Sample {
    id: string;
    species: string;
    purity: number;
  }

  export function analyze(sample: Sample): string {
    return `Sample ${sample.id}: purity ${sample.purity}%`;
  }
}

console.log("--- Namespaces ---");
const cam: Security.Camera = { id: "CAM-01", zone: "T-Rex", isActive: true };
console.log("Camera:", cam.id, "zone:", cam.zone);
console.log("Alert:", Security.describe("red"));

const sample: Genetics.Sample = { id: "DNA-42", species: "Raptor", purity: 87 };
console.log(Genetics.analyze(sample));

// ===========================================
// 2. Nested namespaces
// ===========================================

namespace Park {
  export namespace Monitoring {
    export interface Reading {
      sensorId: string;
      temperature: number;
    }
    export function isNormal(r: Reading): boolean {
      return r.temperature >= 20 && r.temperature <= 35;
    }
  }
  export namespace Feeding {
    export interface Schedule {
      dinoId: string;
      food: string;
      kg: number;
    }
  }
}

console.log("\n--- Nested ---");
const reading: Park.Monitoring.Reading = { sensorId: "T-01", temperature: 28 };
console.log("Sensor:", reading.sensorId, "Normal:", Park.Monitoring.isNormal(reading));

// ===========================================
// 3. Declaration merging - interfaces
// ===========================================

interface ParkDinosaur {
  id: string;
  name: string;
}

interface ParkDinosaur {
  species: string;
  weight: number;
}

interface ParkDinosaur {
  dangerLevel: number;
}

// All fields merged into one interface
const rex: ParkDinosaur = {
  id: "D-001",
  name: "Rexy",
  species: "T-Rex",
  weight: 8000,
  dangerLevel: 10
};

console.log("\n--- Declaration merging ---");
console.log(`${rex.name} (${rex.species}): ${rex.weight}kg, danger ${rex.dangerLevel}/10`);

// ===========================================
// 4. Namespace merging
// ===========================================

namespace ParkSystems {
  export function version(): string { return "1.0"; }
}

namespace ParkSystems {
  export function status(): string { return "Operational"; }
}

console.log("\n--- Namespace merging ---");
console.log("Version:", ParkSystems.version());
console.log("Status:", ParkSystems.status());

// ===========================================
// 5. Namespace + class (merging)
// ===========================================

class Enclosure {
  constructor(public name: string, public capacity: number) {}
}

namespace Enclosure {
  export interface Config {
    fenceVoltage: number;
    cameraCount: number;
  }
  export const DEFAULT_CONFIG: Config = {
    fenceVoltage: 5000,
    cameraCount: 4
  };
}

console.log("\n--- Namespace + class ---");
const enc = new Enclosure("Raptor Zone", 6);
console.log(`Enclosure: ${enc.name}, voltage: ${Enclosure.DEFAULT_CONFIG.fenceVoltage}V`);

// ===========================================
// 6. Practical example
// ===========================================

namespace JP {
  export interface Entity { id: string; }
}

namespace JP {
  export interface Dino extends Entity { name: string; danger: number; }
  export function isDangerous(d: Dino): boolean { return d.danger >= 7; }
}

namespace JP {
  export interface Zone extends Entity { name: string; residents: string[]; }
  export function zoneStatus(z: Zone): string {
    return z.residents.length > 3 ? "Full" : "OK";
  }
}

console.log("\n--- Combined system ---");
const trex: JP.Dino = { id: "D-001", name: "Rexy", danger: 10 };
const zone: JP.Zone = { id: "Z-001", name: "Sector A", residents: ["D-001"] };
console.log(`${trex.name} dangerous: ${JP.isDangerous(trex)}`);
console.log(`${zone.name}: ${JP.zoneStatus(zone)}`);

console.log("\nNamespaces + merging = organizing large systems!");

Docs entries used in this lesson

Spotted a mistake in this lesson?

Useful articles