Kurs JavaScript i TypeScript · Moduł 7: Zaawansowany TypeScript

Namespace, declaration merging i ambient types

7 min czytania ·
W tej lekcji5

"W Parku Jurajskim mamy dziesiątki systemów - monitoring, genetykę, bezpieczeństwo, logistykę," wyjaśnia Dennis Nedry, główny programista parku. "Każdy system ma setki zmiennych, typów, funkcji. Bez porządnej organizacji nazwy zaczęłyby się powtarzać, a chaos stałby się nie do opanowania. Potrzebujemy sposobu na oddzielenie każdego systemu, a jednocześnie pozwolenie im na współpracę."

W TypeScript istnieje kilka mechanizmów organizacji kodu na poziomie typów: namespaces (przestrzenie nazw), declaration merging (łączenie deklaracji) i ambient types (deklaracje otoczenia). Te techniki pozwalają na tworzenie dobrze zorganizowanych, rozszerzalnych systemów typów.

Namespaces - przestrzenie nazw

Namespace to sposób grupowania powiązanych typów, interfejsów, funkcji i zmiennych pod wspólną nazwą. Zapobiega kolizjom nazw i logicznie organizuje kod.

// Namespace dla systemu bezpieczeństwa
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 "Wszystko w normie";
      case "yellow": return "Podwyższona czujność";
      case "red": return "Zagrożenie - ewakuacja gości";
      case "black": return "Krytyczne - pełna ewakuacja";
    }
  }
}

// Namespace dla systemu genetycznego
namespace Genetics {
  export interface Sample {
    id: string;
    species: string;
    purity: number;
  }

  // Ten "Sample" nie koliduje z Security.Camera
  // bo jest w innym namespace
  export function analyzeSample(sample: Sample): string {
    return `Analiza próbki ${sample.id}: czystość ${sample.purity}%`;
  }
}

// Użycie - odwoływanie się przez nazwę namespace
const camera: Security.Camera = { id: "CAM-01", zone: "Strefa T-Rex", isActive: true };
const sample: Genetics.Sample = { id: "DNA-42", species: "Velociraptor", purity: 87 };

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

Zagnieżdżone namespaces

Namespaces mogą być zagnieżdżone, tworząc hierarchiczną strukturę:

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 };
    }
  }
}

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

const schedule = JurassicPark.Feeding.createSchedule("DINO-001", "Koza", 50, "08:00");
console.log("Temperatura normalna:", JurassicPark.Monitoring.isReadingNormal(reading));
console.log("Harmonogram:", JSON.stringify(schedule));

Declaration merging - łączenie deklaracji

Declaration merging to mechanizm TypeScript, w którym kompilator łączy dwie lub więcej osobnych deklaracji o tej samej nazwie w jedną definicję. To szczególnie przydatne do rozszerzania istniejących typów.

Interface merging

Najczęstszym typem declaration merging jest łączenie interfejsów. Gdy zadeklarujesz ten sam interfejs wielokrotnie, TypeScript połączy je w jeden:

// Bazowy interfejs dinozaura
interface ParkDinosaur {
  id: string;
  name: string;
  species: string;
}

// Rozszerzenie tego samego interfejsu - automatycznie się łączą
interface ParkDinosaur {
  weight: number;
  diet: "carnivore" | "herbivore" | "omnivore";
}

// Kolejne rozszerzenie
interface ParkDinosaur {
  enclosureId: string;
  dangerLevel: number;
}

// Wynikowy typ ParkDinosaur ma WSZYSTKIE pola ze wszystkich deklaracji
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, zagrożenie: ${rex.dangerLevel}/10`);

Namespace merging

Namespaces o tej samej nazwie też się łączą:

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

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

// Oba eksporty są dostępne
console.log(ParkSystems.getVersion());
console.log(ParkSystems.getStatus());

Namespace merging z klasą lub funkcją

Namespace może rozszerzać klasę o statyczne właściwości:

class Enclosure {
  name: string;
  capacity: number;

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

// Namespace o tej samej nazwie dodaje statyczne elementy
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);
    // Użyj domyślnej konfiguracji...
    return enc;
  }
}

// Użycie klasy i namespace razem
const enc = new Enclosure("Strefa Raptorów", 6);
const config = Enclosure.DEFAULT_CONFIG;
const secureEnc = Enclosure.createHighSecurity("Strefa T-Rex", 2);

console.log(`Zagroda: ${enc.name}, Domyślne napięcie: ${config.fenceVoltage}V`);

Ambient declarations - deklaracje otoczenia

Słowo kluczowe declare informuje TypeScript, że dana zmienna, funkcja lub klasa istnieje w runtime, ale jest zdefiniowana gdzie indziej (np. w globalnym skrypcie, bibliotece zewnętrznej). Kompilator nie generuje kodu dla declare - tylko sprawdza typy.

// Deklaracja zmiennej globalnej
declare const PARK_VERSION: string;
declare const MAX_DINOSAURS: number;

// Deklaracja funkcji z zewnętrznej biblioteki
declare function trackDinosaur(id: string): { x: number; y: number };
declare function sendAlert(message: string, zone: string): void;

// Deklaracja modułu zewnętrznego
declare module "jurassic-utils" {
  export function calculateFeedingAmount(weight: number, species: string): number;
  export function getEnclosureRecommendation(dangerLevel: number): string;
}

// Deklaracja klasy z zewnętrznego systemu
declare class ExternalTrackingSystem {
  connect(apiKey: string): Promise<void>;
  getPosition(dinoId: string): { lat: number; lng: number };
  disconnect(): void;
}

Pliki .d.ts - pliki deklaracji typów

Pliki z rozszerzeniem .d.ts zawierają WYŁĄCZNIE deklaracje typów - żadnego kodu wykonywalnego. Służą do opisywania typów bibliotek JavaScript, które nie mają wbudowanych typów.

// Przykład pliku jurassic-park.d.ts
// (tylko deklaracje, bez implementacji)

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[]>;
}

Rozszerzanie istniejących modułów (module augmentation)

Możesz dodawać typy do istniejących modułów za pomocą declare module:

// Dodawanie pól do interfejsu Express Request (częsty przypadek)
// Plik: types/express.d.ts
declare module "express" {
  interface Request {
    parkUser?: {
      id: string;
      role: "visitor" | "staff" | "admin";
      clearanceLevel: number;
    };
  }
}

// Rozszerzanie globalnych typów
declare global {
  interface Array<T> {
    // Dodajemy metodę do tablicy
    findDinosaur(predicate: (item: T) => boolean): T | undefined;
  }

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

// Ważne: plik musi mieć przynajmniej jeden import/export,
// by TypeScript traktował go jako moduł
export {};

Praktyczny przykład - organizacja systemu Parku

Poniżej przykład łączący namespaces, declaration merging i ambient declarations w jednym systemie:

// Namespace z typami bazowymi
namespace Park {
  export interface Entity {
    id: string;
    createdAt: Date;
  }

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

// Declaration merging - rozszerzamy Park o nowe typy
namespace Park {
  export interface Dinosaur extends Entity {
    name: string;
    species: string;
    dangerLevel: number;
  }

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

// Kolejne rozszerzenie - funkcje pomocnicze
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 "PEŁNA";
    if (ratio >= 0.75) return "Prawie pełna";
    return "OK";
  }
}

// Użycie - wszystkie rozszerzenia działają razem
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: "Strefa T-Rex",
  capacity: 2,
  residents: ["D-001"]
};

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

Podsumowanie

Mechanizmy organizacji typów w TypeScript to potężne narzędzia do budowania dużych, skalowalnych systemów:

  1. Namespaces - grupowanie powiązanych typów i funkcji, unikanie kolizji nazw
  2. Declaration merging - rozszerzanie istniejących interfejsów i namespace'ów o nowe pola i metody
  3. Ambient declarations (declare) - opisywanie typów dla kodu zdefiniowanego gdzie indziej
  4. Pliki .d.ts - pliki zawierające wyłącznie deklaracje typów (bez kodu)
  5. Module augmentation - rozszerzanie typów w istniejących modułach (np. Express, biblioteki zewnętrzne)

Jak mówi Dennis Nedry: "System musi być zorganizowany jak DNA dinozaura - każdy gen na swoim miejscu, ale wszystkie współpracują w jednym organizmie. Namespaces to chromosomy, declaration merging to ekspresja genów, a ambient types to informacja o środowisku."

Kod do tej lekcji: index.ts
// Namespace, declaration merging, ambient types - Park Jurajski
console.log("=== NAMESPACES I DECLARATION MERGING ===\n");

// ===========================================
// 1. Namespace - organizacja kodu
// ===========================================

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 "Wszystko OK";
      case "yellow": return "Podwyzszona czujnosc";
      case "red": return "ALARM!";
    }
  }
}

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

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

console.log("--- Namespaces ---");
const cam: Security.Camera = { id: "CAM-01", zone: "T-Rex", isActive: true };
console.log("Kamera:", cam.id, "zona:", 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. Zagniezdzone 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--- Zagniezdzone ---");
const reading: Park.Monitoring.Reading = { sensorId: "T-01", temperature: 28 };
console.log("Sensor:", reading.sensorId, "Norma:", Park.Monitoring.isNormal(reading));

// ===========================================
// 3. Declaration merging - interfejsy
// ===========================================

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

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

interface ParkDinosaur {
  dangerLevel: number;
}

// Wszystkie pola polaczone w jeden interfejs
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, zagrozenie ${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("Wersja:", ParkSystems.version());
console.log("Status:", ParkSystems.status());

// ===========================================
// 5. Namespace + klasa (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 + klasa ---");
const enc = new Enclosure("Strefa Raptorow", 6);
console.log(`Zagroda: ${enc.name}, napiecie: ${Enclosure.DEFAULT_CONFIG.fenceVoltage}V`);

// ===========================================
// 6. Praktyczny przyklad
// ===========================================

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 ? "Pelna" : "OK";
  }
}

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

console.log("\nNamespaces + merging = organizacja duzych systemow!");

Hasła z dokumentacji w tej lekcji

Widzisz błąd w tej lekcji?

Przydatne artykuły