Kurs JavaScript i TypeScript · Moduł 7: Zaawansowany TypeScript
Namespace, declaration merging i ambient types
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:
- Namespaces - grupowanie powiązanych typów i funkcji, unikanie kolizji nazw
- Declaration merging - rozszerzanie istniejących interfejsów i namespace'ów o nowe pola i metody
- Ambient declarations (declare) - opisywanie typów dla kodu zdefiniowanego gdzie indziej
- Pliki .d.ts - pliki zawierające wyłącznie deklaracje typów (bez kodu)
- 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?