Cours JavaScript et TypeScript · Module 7: TypeScript avancé

Décorateurs de classe

13 min de lecture
Dans cette leçon9

« À Jurassic Park, chaque bâtiment et chaque infrastructure doivent être correctement étiquetés et sécurisés », explique John Hammond en traversant le Visitor Center fraîchement construit. « Certaines zones sont accessibles à tous, d'autres uniquement au personnel disposant des habilitations appropriées, et d'autres encore nécessitent des protocoles de sécurité spéciaux. Sans un bon système d'étiquettes et de protections, ce serait le chaos. »

En TypeScript, comme à Jurassic Park, nous avons besoin d'un moyen d'étiqueter et de modifier des classes sans toucher à leur implémentation interne. Les décorateurs de classe sont comme des étiquettes spéciales et des protocoles de sécurité que l'on peut appliquer aux classes pour leur ajouter des fonctionnalités ou des caractéristiques supplémentaires.

Introduction aux décorateurs de classe

Les décorateurs de classe permettent de modifier ou d'étendre des classes de manière déclarative. Ils sont particulièrement utiles lorsqu'on veut :

  1. Ajouter des métadonnées à une classe
  2. Enregistrer automatiquement une classe dans un registre
  3. Ajouter des méthodes ou des propriétés à une classe
  4. Modifier le comportement du constructeur
  5. Appliquer des patrons de conception comme Singleton ou Factory

Un décorateur de classe est simplement une fonction qui prend le constructeur d'une classe comme argument et peut le modifier, l'étendre ou le remplacer entièrement.

Syntaxe de base d'un décorateur de classe

Les décorateurs de classe se placent au-dessus de la définition de la classe, précédés du symbole @ :

1@decorator
2class MyClass {
3  // class implementation
4}

Où decorator est une fonction :

1function decorator(target: Function) {
2  // Modification or extension of the class
3}

Exemple : système de surveillance à Jurassic Park

Commençons par un décorateur de classe simple qui marquera les objets nécessitant une surveillance à Jurassic Park :

1// Class decorator
2function Monitored(target: Function) {
3  // Add a property to the class prototype
4  target.prototype.monitored = true;
5
6  // Add a method to the class prototype
7  target.prototype.startMonitoring = function() {
8    console.log(`Monitoring started: ${this.name || this.constructor.name}`);
9  };
10}
11
12// Using the decorator
13@Monitored
14class DinosaurEnclosure {
15  name: string;
16
17  constructor(name: string) {
18    this.name = name;
19  }
20
21  addDinosaur(dinosaur: string) {
22    console.log(`Added ${dinosaur} to enclosure ${this.name}`);
23  }
24}
25
26// Using the class with the decorator
27const raptorEnclosure = new DinosaurEnclosure("Velociraptor");
28raptorEnclosure.addDinosaur("Blue");
29
30// Now we can use the properties and methods added by the decorator
31if ((raptorEnclosure as any).monitored) {
32  (raptorEnclosure as any).startMonitoring();
33}

Dans cet exemple, le décorateur Monitored ajoute une propriété monitored et une méthode startMonitoring au prototype de la classe DinosaurEnclosure. Ainsi, toutes les instances de cette classe auront accès à ces fonctionnalités supplémentaires.

Décorateurs de classe avec paramètres

On peut aussi créer des décorateurs qui acceptent des paramètres. Dans ce cas, notre fonction décoratrice doit renvoyer une autre fonction, qui sera le décorateur proprement dit :

1// Decorator factory
2function SecurityLevel(level: number) {
3  // We return the actual class decorator
4  return function(target: Function) {
5    // Add a property to the class (not to the prototype)
6    target.prototype.securityLevel = level;
7
8    // Add an access verification method
9    target.prototype.checkAccess = function(employeeLevel: number): boolean {
10      return employeeLevel >= level;
11    };
12
13    // We can also override the constructor
14    const originalConstructor = target;
15
16    // Function replacing the constructor
17    function newConstructor(...args: any[]) {
18      console.log(`Creating object with security level: ${level}`);
19
20      // Call the original constructor
21      const instance = new (originalConstructor as any)(...args);
22
23      // Additional logic can be added
24      if (level >= 4) {
25        console.log("WARNING: Creating high security level object!");
26      }
27
28      return instance;
29    }
30
31    // Copy prototype properties
32    newConstructor.prototype = originalConstructor.prototype;
33
34    // Return the new constructor
35    return newConstructor as any;
36  };
37}
38
39// Using the decorator with a parameter
40@SecurityLevel(4)
41class GeneticLaboratory {
42  name: string;
43
44  constructor(name: string) {
45    this.name = name;
46    console.log(`Laboratory created: ${name}`);
47  }
48
49  cloneDinosaur(species: string) {
50    console.log(`Cloning ${species} in laboratory ${this.name}`);
51  }
52}
53
54// Using the class with the decorator
55const laboratory = new GeneticLaboratory("Hammond Lab");
56
57// Checking the security level
58console.log(`Security level: ${(laboratory as any).securityLevel}`);
59
60// Verifying access
61const employeeLevel = 3;
62if ((laboratory as any).checkAccess(employeeLevel)) {
63  console.log("Access granted to laboratory");
64  laboratory.cloneDinosaur("Velociraptor");
65} else {
66  console.log("Access denied! Higher security level required.");
67}

Dans cet exemple, le décorateur SecurityLevel prend un paramètre précisant le niveau d'accès requis. Il ajoute une propriété securityLevel et une méthode checkAccess à la classe, et redéfinit aussi le constructeur pour ajouter une journalisation supplémentaire.

Combiner plusieurs décorateurs

On peut appliquer plusieurs décorateurs à une même classe. Dans ce cas, les décorateurs sont appliqués de bas en haut (du dernier au premier) :

1// Decorator that registers in the system
2function Registered(target: Function) {
3  console.log(`Registering class: ${target.name}`);
4  (target as any).registered = true;
5}
6
7// Decorator that adds logging
8function Logged(prefix: string) {
9  return function(target: Function) {
10    // Save original methods
11    const originalMethods: Record<string, Function> = {};
12
13    // Iterate over prototype methods
14    for (const propertyName of Object.getOwnPropertyNames(target.prototype)) {
15      const descriptor = Object.getOwnPropertyDescriptor(target.prototype, propertyName);
16
17      // Check if it's a method and not a constructor
18      if (descriptor && typeof descriptor.value === 'function' && propertyName !== 'constructor') {
19        originalMethods[propertyName] = descriptor.value;
20
21        // Override the method, adding logging
22        Object.defineProperty(target.prototype, propertyName, {
23          value: function(...args: any[]) {
24            console.log(`${prefix}: Calling ${propertyName} with arguments: ${JSON.stringify(args)}`);
25            const result = originalMethods[propertyName].apply(this, args);
26            console.log(`${prefix}: ${propertyName} returned: ${JSON.stringify(result)}`);
27            return result;
28          }
29        });
30      }
31    }
32  };
33}
34
35// Using multiple decorators
36@Registered
37@SecurityLevel(3)
38@Logged("[Gate control system]")
39class GateControlSystem {
40  state: "open" | "closed" = "closed";
41
42  constructor() {
43    console.log("Initializing gate control system.");
44  }
45
46  openGates(): boolean {
47    console.log("Opening park gates...");
48    this.state = "open";
49    return true;
50  }
51
52  closeGates(): boolean {
53    console.log("Closing park gates...");
54    this.state = "closed";
55    return true;
56  }
57
58  checkStatus(): string {
59    return `Gates are ${this.state}`;
60  }
61}
62
63// Using the class with multiple decorators
64const gateControl = new GateControlSystem();
65gateControl.openGates();
66console.log(gateControl.checkStatus());
67gateControl.closeGates();

Dans cet exemple, la classe GateControlSystem a trois décorateurs appliqués :

  1. @Registered - enregistre la classe dans le système
  2. @SecurityLevel(3) - définit le niveau de sécurité à 3
  3. @Logged("[Gate control system]") - ajoute une journalisation à toutes les méthodes de la classe

Exemple pratique : système de gestion du parc

Voici un exemple plus élaboré d'un système de gestion de Jurassic Park utilisant divers décorateurs de classe :

1// Basic interface for all park resources
2interface ParkResource {
3  id: string;
4  name: string;
5  location: string;
6  status: "active" | "inactive" | "maintenance" | "failure";
7}
8
9// Park resource registry
10class ResourceRegistry {
11  private static registry: Map<string, ParkResource[]> = new Map();
12
13  static registerResource(type: string, resource: ParkResource): void {
14    if (!this.registry.has(type)) {
15      this.registry.set(type, []);
16    }
17
18    this.registry.get(type)!.push(resource);
19    console.log(`Resource registered: ${resource.name} (${type})`);
20  }
21
22  static getResources(type: string): ParkResource[] {
23    return this.registry.get(type) || [];
24  }
25
26  static getAllResources(): Map<string, ParkResource[]> {
27    return new Map(this.registry);
28  }
29}
30
31// Decorator for resource registration
32function RegisterResource(type: string) {
33  return function<T extends { new(...args: any[]): ParkResource }>(constructor: T) {
34    return class extends constructor {
35      constructor(...args: any[]) {
36        super(...args);
37        ResourceRegistry.registerResource(type, this);
38      }
39    };
40  };
41}
42
43// Decorator enforcing audits
44function RequiresAudit(frequencyDays: number) {
45  return function(target: Function) {
46    target.prototype.lastAudit = new Date();
47    target.prototype.auditFrequency = frequencyDays;
48
49    target.prototype.requiresAudit = function(): boolean {
50      const now = new Date();
51      const timeDifference = now.getTime() - this.lastAudit.getTime();
52      const differenceDays = timeDifference / (1000 * 3600 * 24);
53
54      return differenceDays >= this.auditFrequency;
55    };
56
57    target.prototype.performAudit = function(): void {
58      console.log(`Audit conducted for: ${this.name}`);
59      this.lastAudit = new Date();
60    };
61  };
62}
63
64// Decorator adding status history tracking
65function TrackStatuses(target: Function) {
66  // Add status history array
67  target.prototype.statusHistory = [];
68
69  // Save original status setter
70  const originalSetStatus = Object.getOwnPropertyDescriptor(
71    target.prototype, 'status'
72  )?.set;
73
74  // Override the setter for status
75  if (originalSetStatus) {
76    Object.defineProperty(target.prototype, 'status', {
77      set: function(newStatus: "active" | "inactive" | "maintenance" | "failure") {
78        const oldTime = new Date();
79        const oldStatus = this._status;
80
81        // Call the original setter
82        originalSetStatus.call(this, newStatus);
83
84        // Save the change in history
85        this.statusHistory.push({
86          changeTime: oldTime,
87          from: oldStatus,
88          to: newStatus
89        });
90
91        console.log(`Status changed for ${this.name}: ${oldStatus || 'none'} -> ${newStatus}`);
92      },
93      get: Object.getOwnPropertyDescriptor(target.prototype, 'status')?.get
94    });
95  }
96}
97
98// Singleton decorator
99function Singleton<T extends { new(...args: any[]): any }>(constructor: T) {
100  // Save original constructor
101  const originalConstructor = constructor;
102
103  // Replace constructor with a function that controls instance creation
104  const newConstructor: any = function(...args: any[]) {
105    if (!newConstructor.instance) {
106      newConstructor.instance = new originalConstructor(...args);
107    } else {
108      console.warn(`Singleton ${constructor.name} already exists. Returning existing instance.`);
109    }
110
111    return newConstructor.instance;
112  };
113
114  // Copy prototype
115  newConstructor.prototype = originalConstructor.prototype;
116
117  // Store original constructor
118  newConstructor.originalConstructor = originalConstructor;
119
120  return newConstructor;
121}
122
123// Implementation of various park resources
124
125@RegisterResource("enclosure")
126@RequiresAudit(7) // Audit every 7 days
127@TrackStatuses
128class DinosaursEnclosure implements ParkResource {
129  id: string;
130  name: string;
131  location: string;
132  private _status: "active" | "inactive" | "maintenance" | "failure" = "active";
133
134  // Enclosure-specific properties
135  genusDinosaurs: string;
136  capacity: number;
137  currentCount: number;
138
139  constructor(
140    id: string,
141    name: string,
142    location: string,
143    genusDinosaurs: string,
144    capacity: number
145  ) {
146    this.id = id;
147    this.name = name;
148    this.location = location;
149    this.genusDinosaurs = genusDinosaurs;
150    this.capacity = capacity;
151    this.currentCount = 0;
152  }
153
154  get status(): "active" | "inactive" | "maintenance" | "failure" {
155    return this._status;
156  }
157
158  set status(newStatus: "active" | "inactive" | "maintenance" | "failure") {
159    this._status = newStatus;
160  }
161
162  addDinosaur(): boolean {
163    if (this.currentCount < this.capacity && this.status === "active") {
164      this.currentCount++;
165      console.log(`Dinosaur added to enclosure ${this.name}. Current count: ${this.currentCount}`);
166      return true;
167    } else {
168      console.log(`Cannot add dinosaur to enclosure ${this.name}. Enclosure full or inactive.`);
169      return false;
170    }
171  }
172
173  removeDinosaur(): boolean {
174    if (this.currentCount > 0) {
175      this.currentCount--;
176      console.log(`Dinosaur removed from enclosure ${this.name}. Current count: ${this.currentCount}`);
177      return true;
178    } else {
179      console.log(`Cannot remove dinosaur from enclosure ${this.name}. Enclosure empty.`);
180      return false;
181    }
182  }
183}
184
185@RegisterResource("building")
186@RequiresAudit(30) // Audit every 30 days
187@TrackStatuses
188class ParkBuilding implements ParkResource {
189  id: string;
190  name: string;
191  location: string;
192  private _status: "active" | "inactive" | "maintenance" | "failure" = "active";
193
194  // Building-specific properties
195  genusBuilding: "public" | "administration" | "laboratory" | "maintenance";
196  area: number;
197
198  constructor(
199    id: string,
200    name: string,
201    location: string,
202    genusBuilding: "public" | "administration" | "laboratory" | "maintenance",
203    area: number
204  ) {
205    this.id = id;
206    this.name = name;
207    this.location = location;
208    this.genusBuilding = genusBuilding;
209    this.area = area;
210  }
211
212  get status(): "active" | "inactive" | "maintenance" | "failure" {
213    return this._status;
214  }
215
216  set status(newStatus: "active" | "inactive" | "maintenance" | "failure") {
217    this._status = newStatus;
218  }
219
220  open(): void {
221    if (this.status !== "active") {
222      console.log(`Cannot open building ${this.name}. Status: ${this.status}`);
223      return;
224    }
225
226    console.log(`Building ${this.name} opened`);
227  }
228
229  close(): void {
230    console.log(`Building ${this.name} closed`);
231  }
232}
233
234@Singleton
235@RegisterResource("system")
236class SecuritySystem implements ParkResource {
237  id: string;
238  name: string;
239  location: string;
240  private _status: "active" | "inactive" | "maintenance" | "failure" = "active";
241
242  // Security system-specific properties
243  emergencyProtocol: "normall" | "warning" | "threat" | "evacuation" = "normall";
244
245  constructor() {
246    this.id = "SYS-SEC-001";
247    this.name = "Main Security System";
248    this.location = "Control center";
249  }
250
251  get status(): "active" | "inactive" | "maintenance" | "failure" {
252    return this._status;
253  }
254
255  set status(newStatus: "active" | "inactive" | "maintenance" | "failure") {
256    this._status = newStatus;
257
258    if (newStatus !== "active" && this.emergencyProtocol === "normall") {
259      console.warn("WARNING: Security system is not active!");
260      this.setProtocol("warning");
261    }
262  }
263
264  setProtocol(protocol: "normall" | "warning" | "threat" | "evacuation"): void {
265    this.emergencyProtocol = protocol;
266    console.log(`Security protocol set to: ${protocol}`);
267
268    // Action implementation depending on the protocol
269    switch (protocol) {
270      case "normall":
271        console.log("All systems operating normallly.");
272        break;
273      case "warning":
274        console.log("Increased staff alertness. Checking all systems.");
275        break;
276      case "threat":
277        console.log("WARNING: Threat detected! Staff to positions! Prepare for evacuation.");
278        break;
279      case "evacuation":
280        console.log("WARNING! WARNING! Immediate park evacuation! This is not a drill!");
281        break;
282    }
283  }
284
285  checkStatus(): void {
286    console.log(`Security system status: ${this.status}`);
287    console.log(`Current protocol: ${this.emergencyProtocol}`);
288  }
289}
290
291// Using classes with decorators
292
293// Creating an enclosure
294const enclosureT = new DinosaursEnclosure(
295  "ZAG-001",
296  "T-Rex Enclosure",
297  "Sector A",
298  "Tyrannosaurus Rex",
299  2
300);
301
302enclosureT.addDinosaur();
303enclosureT.status = "maintenance";
304enclosureT.status = "active";
305
306// Creating a building
307const visitorCenter = new ParkBuilding(
308  "BUD-001",
309  "Visitor Center",
310  "Main entrance",
311  "public",
312  2500
313);
314
315visitorCenter.open();
316visitorCenter.status = "maintenance";
317visitorCenter.status = "active";
318
319// Creating the security system (Singleton)
320const securitySystem1 = new SecuritySystem();
321securitySystem1.checkStatus();
322securitySystem1.setProtocol("warning");
323
324// Attempting to create a second security system
325const securitySystem2 = new SecuritySystem();
326// Note that securitySystem1 === securitySystem2
327
328// Checking audits
329console.log(`T-Rex Enclosure requires audit: ${(enclosureT as any).requiresAudit()}`);
330console.log(`Visitor Center requires audit: ${(visitorCenter as any).requiresAudit()}`);
331
332// Conducting audit
333if ((enclosureT as any).requiresAudit()) {
334  (enclosureT as any).performAudit();
335}
336
337// Getting all resources from registry
338console.log("Park resource list:");
339const allResources = ResourceRegistry.getAllResources();
340for (const [type, resources] of allResources) {
341  console.log(`Type: ${type}, Count: ${resources.length}`);
342  resources.forEach(resource => {
343    console.log(`  - ${resource.name} (${resource.status})`);
344  });
345}
346
347// Checking status history
348console.log("T-Rex Enclosure status history:");
349(enclosureT as any).statusHistory.forEach((change: any) => {
350  console.log(`  ${change.changeTime.toLocaleString()}: ${change.from || 'initial'} -> ${change.to}`);
351});

Cet exemple élaboré illustre une utilisation avancée des décorateurs de classe :

  1. @RegisterResource - enregistre automatiquement les ressources dans un registre central
  2. @RequiresAudit - ajoute une fonctionnalité de suivi du calendrier des audits
  3. @TrackStatuses - ajoute un historique des changements de statut
  4. @Singleton - garantit qu'une seule instance d'une classe peut être créée

Quand utiliser les décorateurs de classe

Les décorateurs de classe sont particulièrement utiles :

  1. Lorsque tu veux ajouter une fonctionnalité transversale - lorsque la même fonctionnalité doit être ajoutée à de nombreuses classes (programmation orientée aspect)
  2. Lors de l'implémentation de patrons de conception - tels que Singleton, Factory, Observer
  3. Pour l'enregistrement automatique - par ex. enregistrer des composants dans un système d'injection de dépendances
  4. Pour la métaprogrammation - lorsque le code doit se modifier lui-même à l'exécution
  5. Pour ajouter des métadonnées - qui pourront ensuite être utilisées par un framework

Limites et problèmes des décorateurs de classe

Les décorateurs de classe ont aussi certaines limites et certains problèmes :

  1. Débogage plus difficile - un code comportant de nombreux décorateurs peut être plus difficile à déboguer
  2. Problèmes de typage - il faut parfois utiliser as any pour contourner des problèmes de TypeScript
  3. Fonctionnalité cachée - les décorateurs peuvent masquer des fonctionnalités importantes, rendant le code plus difficile à comprendre
  4. Ordre d'application - l'ordre d'application des décorateurs a son importance et peut poser problème
  5. Évolutions de la spécification - la spécification des décorateurs en JavaScript et TypeScript continue d'évoluer

Résumé

Les décorateurs de classe en TypeScript sont un outil puissant qui permet d'ajouter, de modifier ou de remplacer des fonctionnalités d'une classe de manière déclarative, sans toucher à son implémentation interne. Tout comme les protocoles de sécurité spéciaux de Jurassic Park aident à maintenir l'ordre et le contrôle, les décorateurs de classe aident à organiser et à étendre le code de façon claire et systématique.

Les décorateurs de classe sont surtout utiles dans les grandes applications où il faut souvent implémenter une fonctionnalité transversale à travers de nombreuses classes, comme la journalisation, l'autorisation, la validation ou l'enregistrement de composants. Grâce à eux, nous pouvons créer un code plus modulaire, flexible et facile à maintenir.

Souviens-toi cependant que, comme tout outil puissant, les décorateurs de classe doivent être utilisés avec discernement. En abuser peut conduire à un code difficile à déboguer et à comprendre. Utilise-les toujours lorsqu'ils résolvent réellement un problème, et non simplement parce qu'ils ont l'air élégants.

Code de cette leçon : index.ts
1// Système de types complet du parc - Parc Jurassique
2console.log("=== SYSTÈME DE TYPES AVANCÉ DU PARC ===\n");
3
4// ===========================================
5// 1. Types et interfaces de base
6// ===========================================
7
8interface Entity {
9  readonly id: string;
10  readonly createdAt: Date;
11}
12
13interface Dinosaur extends Entity {
14  name: string;
15  species: string;
16  weight: number;
17  diet: "carnivore" | "herbivore" | "omnivore";
18  dangerLevel: number;
19}
20
21interface Enclosure extends Entity {
22  name: string;
23  capacity: number;
24  securityLevel: number;
25  residents: string[];
26}
27
28interface StaffMember extends Entity {
29  name: string;
30  role: "ranger" | "veterinarian" | "researcher" | "security";
31  clearanceLevel: number;
32}
33
34// ===========================================
35// 2. Types mappés et conditionnels
36// ===========================================
37
38type EntityMap = {
39  dinosaur: Dinosaur;
40  enclosure: Enclosure;
41  staff: StaffMember;
42};
43
44type EntityType = keyof EntityMap;
45
46// Création automatique des types DTO
47type CreateDTO<T extends Entity> = Omit<T, "id" | "createdAt">;
48type UpdateDTO<T extends Entity> = Partial<Omit<T, "id" | "createdAt">>;
49type ReadDTO<T extends Entity> = Readonly<T>;
50
51// ===========================================
52// 3. Dépôt générique avec contraintes
53// ===========================================
54
55class Repository<T extends Entity> {
56  private storage: Map<string, T> = new Map();
57  private counter = 0;
58  private prefix: string;
59
60  constructor(prefix: string) {
61    this.prefix = prefix;
62  }
63
64  create(data: CreateDTO<T>): T {
65    const id = `${this.prefix}-${String(++this.counter).padStart(3, "0")}`;
66    const entity = { ...data, id, createdAt: new Date() } as T;
67    this.storage.set(id, entity);
68    return entity;
69  }
70
71  findById(id: string): ReadDTO<T> | undefined {
72    return this.storage.get(id);
73  }
74
75  findAll(): ReadDTO<T>[] {
76    return Array.from(this.storage.values());
77  }
78
79  update(id: string, data: UpdateDTO<T>): T | undefined {
80    const existing = this.storage.get(id);
81    if (!existing) return undefined;
82    const updated = { ...existing, ...data } as T;
83    this.storage.set(id, updated);
84    return updated;
85  }
86
87  delete(id: string): boolean {
88    return this.storage.delete(id);
89  }
90
91  query(predicate: (item: T) => boolean): ReadDTO<T>[] {
92    return this.findAll().filter(item => predicate(item as T));
93  }
94}
95
96// ===========================================
97// 4. Système d'événements avec types conditionnels
98// ===========================================
99
100type EventSeverity = "info" | "warning" | "critical";
101
102type ParkEvent<S extends EventSeverity> = S extends "critical"
103  ? { severity: S; message: string; requiresEvacuation: boolean; affectedZones: string[] }
104  : S extends "warning"
105  ? { severity: S; message: string; affectedZones: string[] }
106  : { severity: S; message: string };
107
108function createEvent<S extends EventSeverity>(
109  severity: S,
110  data: Omit<ParkEvent<S>, "severity">
111): ParkEvent<S> {
112  return { severity, ...data } as ParkEvent<S>;
113}
114
115// ===========================================
116// 5. Système de rapports avec types mappés
117// ===========================================
118
119type DinoReport = {
120  [K in keyof Omit<Dinosaur, "id" | "createdAt">]: {
121    label: string;
122    value: Dinosaur[K];
123    formatted: string;
124  };
125};
126
127function generateReport(dino: Dinosaur): DinoReport {
128  return {
129    name: { label: "Nom", value: dino.name, formatted: dino.name },
130    species: { label: "Espèce", value: dino.species, formatted: dino.species },
131    weight: { label: "Poids", value: dino.weight, formatted: `${dino.weight} kg` },
132    diet: { label: "Régime", value: dino.diet, formatted: dino.diet === "carnivore" ? "Carnivore" : dino.diet === "herbivore" ? "Herbivore" : "Omnivore" },
133    dangerLevel: { label: "Danger", value: dino.dangerLevel, formatted: `${dino.dangerLevel}/10` }
134  };
135}
136
137// ===========================================
138// Test
139// ===========================================
140
141// Dépôts
142const dinoRepo = new Repository<Dinosaur>("DINO");
143const encRepo = new Repository<Enclosure>("ENC");
144const staffRepo = new Repository<StaffMember>("STAFF");
145
146// Création des dinosaures
147console.log("--- Création des entités ---");
148const rex = dinoRepo.create({ name: "Rexy", species: "T-Rex", weight: 8000, diet: "carnivore", dangerLevel: 10 });
149const blue = dinoRepo.create({ name: "Blue", species: "Velociraptor", weight: 15, diet: "carnivore", dangerLevel: 8 });
150const patty = dinoRepo.create({ name: "Patty", species: "Brachiosaurus", weight: 50000, diet: "herbivore", dangerLevel: 1 });
151
152const mainEnc = encRepo.create({ name: "Zone T-Rex", capacity: 2, securityLevel: 10, residents: [rex.id] });
153const grant = staffRepo.create({ name: "Alan Grant", role: "researcher", clearanceLevel: 8 });
154
155console.log(`Dinosaur: ${rex.id} - ${rex.name}`);
156console.log(`Enclos: ${mainEnc.id} - ${mainEnc.name}`);
157console.log(`Employé: ${grant.id} - ${grant.name}`);
158
159// Mise à jour
160console.log("\n--- Mise à jour ---");
161const updatedRex = dinoRepo.update(rex.id, { weight: 8200 });
162console.log(`${updatedRex?.name}: ${updatedRex?.weight}kg`);
163
164// Requête
165console.log("\n--- Requêtes ---");
166const dangerousDinos = dinoRepo.query(d => d.dangerLevel >= 7);
167console.log("Dangereux:", dangerousDinos.map(d => d.name).join(", "));
168
169const carnivores = dinoRepo.query(d => d.diet === "carnivore");
170console.log("Carnivores:", carnivores.map(d => d.name).join(", "));
171
172// Événements
173console.log("\n--- Événements ---");
174const info = createEvent("info", { message: "Contrôle de routine terminé" });
175console.log("Info:", info.message);
176
177const warning = createEvent("warning", {
178  message: "Fence needs repair",
179  affectedZones: ["Zone A"]
180});
181console.log("Avertissement:", warning.message, "Zones:", warning.affectedZones.join(", "));
182
183const critical = createEvent("critical", {
184  message: "Évasion de dinosaure !",
185  requiresEvacuation: true,
186  affectedZones: ["Zone A", "Zone B"]
187});
188console.log("CRITIQUE:", critical.message, "Évacuation:", critical.requiresEvacuation);
189
190// Rapport
191console.log("\n--- Rapport du dinosaure ---");
192const report = generateReport(rex);
193Object.values(report).forEach(field => {
194  console.log(`  ${field.label}: ${field.formatted}`);
195});
196
197console.log("\nSystème de types TypeScript complet en action!");

Tu as repéré une erreur dans cette leçon ?

Teste tes connaissances

Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.

  1. 1. Quel type utilitaire sert à EXCLURE des types précis d'un type union ?

  2. 2. Qu'est-ce qu'un décorateur en TypeScript ?

Tâches pratiques dans le jeu

  • Tri vertical

    Ordonne les étapes de création d'un type utilitaire personnalisé en TypeScript :

  • Clique dans l'ordre

    Range les éléments de la déclaration d'un décorateur de classe :

Articles utiles