Cours JavaScript et TypeScript · Module 7: TypeScript avancé
Espaces de noms, fusion de déclarations et types ambiants
Dans cette leçon5
« À Jurassic Park, nous avons des dizaines de systèmes - surveillance, génétique, sécurité, logistique », explique Dennis Nedry, le programmeur principal du parc. « Chaque système comporte des centaines de variables, de types, de fonctions. Sans une organisation appropriée, les noms commenceraient à se répéter, et le chaos deviendrait ingérable. Il nous faut un moyen de séparer chaque système tout en leur permettant de coopérer. »
En TypeScript, il existe plusieurs mécanismes pour organiser le code au niveau des types : les espaces de noms (namespaces), la fusion de déclarations et les types ambiants. Ces techniques permettent de construire des systèmes de types bien organisés et extensibles.
Espaces de noms (namespaces)
Un espace de noms est un moyen de regrouper sous un nom commun des types, interfaces, fonctions et variables apparentés. Il évite les collisions de noms et organise logiquement le code.
1// Namespace for the security system
2namespace Security {
3 export interface Camera {
4 id: string;
5 zone: string;
6 isActive: boolean;
7 }
8
9 export interface Guard {
10 id: string;
11 name: string;
12 assignedZone: string;
13 }
14
15 export type AlertLevel = "green" | "yellow" | "red" | "black";
16
17 export function getAlertDescription(level: AlertLevel): string {
18 switch (level) {
19 case "green": return "All normall";
20 case "yellow": return "Elevated vigilance";
21 case "red": return "Threat - evacuate visitors";
22 case "black": return "Critical - full evacuation";
23 }
24 }
25}
26
27// Namespace for the genetics system
28namespace Genetics {
29 export interface Sample {
30 id: string;
31 species: string;
32 purity: number;
33 }
34
35 // This "Sample" does not collide with Security.Camera
36 // because it's in a different namespace
37 export function analyzeSample(sample: Sample): string {
38 return `Sample analysis ${sample.id}: purity ${sample.purity}%`;
39 }
40}
41
42// Usage - referencing through the namespace name
43const camera: Security.Camera = { id: "CAM-01", zone: "T-Rex Zone", isActive: true };
44const sample: Genetics.Sample = { id: "DNA-42", species: "Velociraptor", purity: 87 };
45
46console.log(Security.getAlertDescription("red"));
47console.log(Genetics.analyzeSample(sample));Espaces de noms imbriqués
Les espaces de noms peuvent être imbriqués, créant une structure hiérarchique :
1namespace JurassicPark {
2 export namespace Monitoring {
3 export interface SensorReading {
4 sensorId: string;
5 temperature: number;
6 humidity: number;
7 timestamp: Date;
8 }
9
10 export function isReadingNormal(reading: SensorReading): boolean {
11 return reading.temperature >= 20 && reading.temperature <= 35;
12 }
13 }
14
15 export namespace Feeding {
16 export interface Schedule {
17 dinosaurId: string;
18 food: string;
19 amountKg: number;
20 time: string;
21 }
22
23 export function createSchedule(dinoId: string, food: string, kg: number, time: string): Schedule {
24 return { dinosaurId: dinoId, food, amountKg: kg, time };
25 }
26 }
27}
28
29// Usage
30const reading: JurassicPark.Monitoring.SensorReading = {
31 sensorId: "TEMP-A3",
32 temperature: 28,
33 humidity: 65,
34 timestamp: new Date()
35};
36
37const schedule = JurassicPark.Feeding.createSchedule("DINO-001", "Goat", 50, "08:00");
38console.log("Temperature normall:", JurassicPark.Monitoring.isReadingNormal(reading));
39console.log("Schedule:", JSON.stringify(schedule));Fusion de déclarations
La fusion de déclarations est un mécanisme de TypeScript par lequel le compilateur combine deux déclarations distinctes ou plus portant le même nom en une seule définition. C'est particulièrement utile pour étendre des types existants.
Fusion d'interfaces
Le type le plus courant de fusion de déclarations est la fusion d'interfaces. Lorsque tu déclares plusieurs fois la même interface, TypeScript les fusionne en une seule :
1// Base dinosaur interface
2interface ParkDinosaur {
3 id: string;
4 name: string;
5 species: string;
6}
7
8// Extending the same interface - automatically merged
9interface ParkDinosaur {
10 weight: number;
11 diet: "carnivore" | "herbivore" | "omnivore";
12}
13
14// Another extension
15interface ParkDinosaur {
16 enclosureId: string;
17 dangerLevel: number;
18}
19
20// The resulting ParkDinosaur type has ALL fields from all declarations
21const rex: ParkDinosaur = {
22 id: "D-001",
23 name: "Rexy",
24 species: "Tyrannosaurus Rex",
25 weight: 8000,
26 diet: "carnivore",
27 enclosureId: "ENC-A1",
28 dangerLevel: 10
29};
30
31console.log(`${rex.name}: ${rex.weight}kg, danger: ${rex.dangerLevel}/10`);Fusion d'espaces de noms
Les espaces de noms portant le même nom fusionnent eux aussi :
1namespace ParkSystems {
2 export function getVersion(): string {
3 return "1.0.0";
4 }
5}
6
7namespace ParkSystems {
8 export function getStatus(): string {
9 return "Operational";
10 }
11}
12
13// Both exports are available
14console.log(ParkSystems.getVersion());
15console.log(ParkSystems.getStatus());Fusion d'un espace de noms avec une classe ou une fonction
Un espace de noms peut étendre une classe avec des propriétés statiques :
1class Enclosure {
2 name: string;
3 capacity: number;
4
5 constructor(name: string, capacity: number) {
6 this.name = name;
7 this.capacity = capacity;
8 }
9}
10
11// A namespace with the same name adds static members
12namespace Enclosure {
13 export interface Config {
14 fenceVoltage: number;
15 cameraCount: number;
16 guardCount: number;
17 }
18
19 export const DEFAULT_CONFIG: Config = {
20 fenceVoltage: 5000,
21 cameraCount: 4,
22 guardCount: 2
23 };
24
25 export function createHighSecurity(name: string, capacity: number): Enclosure {
26 const enc = new Enclosure(name, capacity);
27 // Use the default configuration...
28 return enc;
29 }
30}
31
32// Using the class and namespace together
33const enc = new Enclosure("Raptor Zone", 6);
34const config = Enclosure.DEFAULT_CONFIG;
35const secureEnc = Enclosure.createHighSecurity("T-Rex Zone", 2);
36
37console.log(`Enclosure: ${enc.name}, Default voltage: ${config.fenceVoltage}V`);Déclarations ambiantes
Le mot-clé declare indique à TypeScript qu'une variable, une fonction ou une classe donnée existe à l'exécution mais est définie ailleurs (par ex. dans un script global, une bibliothèque externe). Le compilateur ne génère pas de code pour declare - il se contente de vérifier les types.
1// Global variable declaration
2declare const PARK_VERSION: string;
3declare const MAX_DINOSAURS: number;
4
5// Function declaration from an external library
6declare function trackDinosaur(id: string): { x: number; y: number };
7declare function sendAlert(message: string, zone: string): void;
8
9// External module declaration
10declare module "jurassic-utils" {
11 export function calculateFeedingAmount(weight: number, species: string): number;
12 export function getEnclosureRecommendation(dangerLevel: number): string;
13}
14
15// Class declaration from an external system
16declare class ExternalTrackingSystem {
17 connect(apiKey: string): Promise<void>;
18 getPosition(dinoId: string): { lat: number; lng: number };
19 disconnect(): void;
20}Fichiers .d.ts - fichiers de déclaration de types
Les fichiers portant l'extension .d.ts ne contiennent QUE des déclarations de types - aucun code exécutable. Ils servent à décrire les types des bibliothèques JavaScript qui n'ont pas de types intégrés.
1// Example jurassic-park.d.ts file
2// (declarations only, no implementation)
3
4declare namespace JurassicParkAPI {
5 interface DinosaurResponse {
6 id: string;
7 name: string;
8 species: string;
9 location: { lat: number; lng: number };
10 }
11
12 interface EnclosureResponse {
13 id: string;
14 name: string;
15 status: "normall" | "lockdown" | "breach";
16 dinosaurIds: string[];
17 }
18
19 function fetchDinosaur(id: string): Promise<DinosaurResponse>;
20 function fetchEnclosure(id: string): Promise<EnclosureResponse>;
21 function getAllDinosaurs(): Promise<DinosaurResponse[]>;
22}Étendre des modules existants (augmentation de module)
Tu peux ajouter des types à des modules existants à l'aide de declare module :
1// Adding fields to the Express Request interface (common case)
2// File: types/express.d.ts
3declare module "express" {
4 interface Request {
5 parkUser?: {
6 id: string;
7 role: "visitor" | "staff" | "admin";
8 clearanceLevel: number;
9 };
10 }
11}
12
13// Extending global types
14declare global {
15 interface Array<T> {
16 // Add a method to Array
17 findDinosaur(predicate: (item: T) => boolean): T | undefined;
18 }
19
20 interface Window {
21 parkConfig: {
22 apiUrl: string;
23 version: string;
24 debugMode: boolean;
25 };
26 }
27}
28
29// Important: the file must have at least one import/export
30// for TypeScript to treat it as a module
31export {};Exemple pratique - organiser le système du parc
Voici un exemple combinant espaces de noms, fusion de déclarations et déclarations ambiantes dans un même système :
1// Namespace with base types
2namespace Park {
3 export interface Entity {
4 id: string;
5 createdAt: Date;
6 }
7
8 export type Priority = "low" | "medium" | "high" | "critical";
9}
10
11// Declaration merging - extending Park with new types
12namespace Park {
13 export interface Dinosaur extends Entity {
14 name: string;
15 species: string;
16 dangerLevel: number;
17 }
18
19 export interface Enclosure extends Entity {
20 name: string;
21 capacity: number;
22 residents: string[];
23 }
24}
25
26// Another extension - helper functions
27namespace Park {
28 export function isDangerous(dino: Dinosaur): boolean {
29 return dino.dangerLevel >= 7;
30 }
31
32 export function getEnclosureStatus(enc: Enclosure): string {
33 const ratio = enc.residents.length / enc.capacity;
34 if (ratio >= 1) return "FULL";
35 if (ratio >= 0.75) return "Almost full";
36 return "OK";
37 }
38}
39
40// Usage - all extensions work together
41const trex: Park.Dinosaur = {
42 id: "D-001",
43 createdAt: new Date(),
44 name: "Rexy",
45 species: "T-Rex",
46 dangerLevel: 10
47};
48
49const enclosure: Park.Enclosure = {
50 id: "E-001",
51 createdAt: new Date(),
52 name: "T-Rex Zone",
53 capacity: 2,
54 residents: ["D-001"]
55};
56
57console.log(`${trex.name} dangerous: ${Park.isDangerous(trex)}`);
58console.log(`Enclosure ${enclosure.name}: ${Park.getEnclosureStatus(enclosure)}`);Résumé
Les mécanismes d'organisation des types en TypeScript sont des outils puissants pour construire de grands systèmes évolutifs :
- Espaces de noms - regrouper des types et des fonctions apparentés, éviter les collisions de noms
- Fusion de déclarations - étendre des interfaces et des espaces de noms existants avec de nouveaux champs et méthodes
- Déclarations ambiantes (declare) - décrire les types d'un code défini ailleurs
- Fichiers .d.ts - des fichiers contenant uniquement des déclarations de types (aucun code)
- Augmentation de module - étendre les types de modules existants (par ex. Express, bibliothèques externes)
Comme le dit Dennis Nedry : « Le système doit être organisé comme l'ADN d'un dinosaure - chaque gène à sa place, mais tous coopérant au sein d'un même organisme. Les espaces de noms sont les chromosomes, la fusion de déclarations est l'expression des gènes, et les types ambiants sont l'information sur l'environnement. »
Code de cette leçon : index.ts
1// Namespace, declaration merging, ambient types - Parc Jurassique
2console.log("=== NAMESPACES ET DECLARATION MERGING ===\n");
3
4// ===========================================
5// 1. Namespace - organisation du code
6// ===========================================
7
8namespace Security {
9 export interface Camera {
10 id: string;
11 zone: string;
12 isActive: boolean;
13 }
14
15 export type AlertLevel = "green" | "yellow" | "red";
16
17 export function describe(level: AlertLevel): string {
18 switch (level) {
19 case "green": return "Tout va bien";
20 case "yellow": return "Vigilance accrue";
21 case "red": return "ALARM!";
22 }
23 }
24}
25
26namespace Genetics {
27 export interface Sample {
28 id: string;
29 species: string;
30 purity: number;
31 }
32
33 export function analyze(sample: Sample): string {
34 return `Échantillon ${sample.id}: pureté ${sample.purity}%`;
35 }
36}
37
38console.log("--- Namespaces ---");
39const cam: Security.Camera = { id: "CAM-01", zone: "T-Rex", isActive: true };
40console.log("Caméra:", cam.id, "zone:", cam.zone);
41console.log("Alert:", Security.describe("red"));
42
43const sample: Genetics.Sample = { id: "DNA-42", species: "Raptor", purity: 87 };
44console.log(Genetics.analyze(sample));
45
46// ===========================================
47// 2. Namespaces imbriqués
48// ===========================================
49
50namespace Park {
51 export namespace Monitoring {
52 export interface Reading {
53 sensorId: string;
54 temperature: number;
55 }
56 export function isNormal(r: Reading): boolean {
57 return r.temperature >= 20 && r.temperature <= 35;
58 }
59 }
60 export namespace Feeding {
61 export interface Schedule {
62 dinoId: string;
63 food: string;
64 kg: number;
65 }
66 }
67}
68
69console.log("\n--- Imbriqués ---");
70const reading: Park.Monitoring.Reading = { sensorId: "T-01", temperature: 28 };
71console.log("Sensor:", reading.sensorId, "Normal:", Park.Monitoring.isNormal(reading));
72
73// ===========================================
74// 3. Fusion de déclarations - interfaces
75// ===========================================
76
77interface ParkDinosaur {
78 id: string;
79 name: string;
80}
81
82interface ParkDinosaur {
83 species: string;
84 weight: number;
85}
86
87interface ParkDinosaur {
88 dangerLevel: number;
89}
90
91// Tous les champs réunis en une seule interface
92const rex: ParkDinosaur = {
93 id: "D-001",
94 name: "Rexy",
95 species: "T-Rex",
96 weight: 8000,
97 dangerLevel: 10
98};
99
100console.log("\n--- Declaration merging ---");
101console.log(`${rex.name} (${rex.species}): ${rex.weight}kg, danger ${rex.dangerLevel}/10`);
102
103// ===========================================
104// 4. Fusion de namespaces
105// ===========================================
106
107namespace ParkSystems {
108 export function version(): string { return "1.0"; }
109}
110
111namespace ParkSystems {
112 export function status(): string { return "Operational"; }
113}
114
115console.log("\n--- Namespace merging ---");
116console.log("Version:", ParkSystems.version());
117console.log("Status:", ParkSystems.status());
118
119// ===========================================
120// 5. Namespace + classe (merging)
121// ===========================================
122
123class Enclosure {
124 constructor(public name: string, public capacity: number) {}
125}
126
127namespace Enclosure {
128 export interface Config {
129 fenceVoltage: number;
130 cameraCount: number;
131 }
132 export const DEFAULT_CONFIG: Config = {
133 fenceVoltage: 5000,
134 cameraCount: 4
135 };
136}
137
138console.log("\n--- Namespace + classe ---");
139const enc = new Enclosure("Zone des Raptors", 6);
140console.log(`Enclosure: ${enc.name}, tension: ${Enclosure.DEFAULT_CONFIG.fenceVoltage}V`);
141
142// ===========================================
143// 6. Exemple pratique
144// ===========================================
145
146namespace JP {
147 export interface Entity { id: string; }
148}
149
150namespace JP {
151 export interface Dino extends Entity { name: string; danger: number; }
152 export function isDangerous(d: Dino): boolean { return d.danger >= 7; }
153}
154
155namespace JP {
156 export interface Zone extends Entity { name: string; residents: string[]; }
157 export function zoneStatus(z: Zone): string {
158 return z.residents.length > 3 ? "Pleine" : "OK";
159 }
160}
161
162console.log("\n--- Système combiné ---");
163const trex: JP.Dino = { id: "D-001", name: "Rexy", danger: 10 };
164const zone: JP.Zone = { id: "Z-001", name: "Secteur A", residents: ["D-001"] };
165console.log(`${trex.name} dangereux: ${JP.isDangerous(trex)}`);
166console.log(`${zone.name}: ${JP.zoneStatus(zone)}`);
167
168console.log("\nNamespaces + merging = organisation des grands systèmes !");Tu as repéré une erreur dans cette leçon ?