Cours JavaScript et TypeScript · Module 7: TypeScript avancé

Décorateurs de méthode et de propriété

20 min de lecture
Dans cette leçon12

« La supervision des opérations individuelles à Jurassic Park est tout aussi importante que la gestion du parc dans son ensemble », explique le Dr Henry Wu, généticien en chef d'InGen. « Tout comme nous surveillons chaque aspect de la vie des dinosaures - leur alimentation, leur comportement et leur croissance - nous devons aussi contrôler et sécuriser les opérations individuelles dans notre système. »

En TypeScript, après avoir compris les décorateurs de classe, l'étape suivante naturelle consiste à approfondir les décorateurs de méthode et de propriété. Ces décorateurs nous permettent de modifier précisément le comportement de membres spécifiques d'une classe, tout comme à Jurassic Park, où chaque opération, du nourrissage des dinosaures à la sécurisation des enclos, requiert une supervision et des protocoles particuliers.

Introduction aux décorateurs de méthode et de propriété

Les décorateurs de méthode et de propriété permettent de modifier le comportement, l'accès ou la valeur de membres individuels d'une classe. Grâce à eux, nous pouvons :

  1. Ajouter un comportement supplémentaire avant ou après l'exécution d'une méthode
  2. Valider les paramètres d'une méthode
  3. Mettre en cache les résultats d'une méthode
  4. Mesurer le temps d'exécution d'une méthode
  5. Contrôler l'accès à une propriété
  6. Transformer les valeurs d'une propriété
  7. Ajouter des métadonnées aux méthodes et aux propriétés

Décorateurs de méthode

Les décorateurs de méthode sont des fonctions qui prennent trois paramètres :

  1. target - le prototype de la classe (pour les méthodes d'instance) ou le constructeur de la classe (pour les méthodes statiques)
  2. propertyKey - le nom de la méthode
  3. descriptor - le descripteur de propriété de la méthode

Syntaxe de base d'un décorateur de méthode :

1function methodDecorator(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
2  // Modification of the method descriptor
3  // Return the modified descriptor or undefined
4}
5
6class MyClass {
7  @methodDecorator
8  myMethod() {
9    // Method implementation
10  }
11}

Exemple : système de journalisation des opérations à Jurassic Park

Commençons par un décorateur de méthode simple qui journalisera l'exécution des opérations à Jurassic Park :

1// Method decorator that logs calls
2function LogOperation(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
3  // Save the original method
4  const originalMethod = descriptor.value;
5
6  // Override the method
7  descriptor.value = function(...args: any[]) {
8    console.log(`[${new Date().toISOString()}] Calling ${propertyKey} with arguments: ${JSON.stringify(args)}`);
9
10    try {
11      // Call the original method
12      const result = originalMethod.apply(this, args);
13
14      // Log the result
15      console.log(`[${new Date().toISOString()}] ${propertyKey} completed. Result: ${JSON.stringify(result)}`);
16
17      return result;
18    } catch (error) {
19      // Log the error
20      console.error(`[${new Date().toISOString()}] ${propertyKey} failed with error: ${error}`);
21
22      // Re-throw the error
23      throw error;
24    }
25  };
26
27  return descriptor;
28}
29
30// Applying the decorator in the enclosure management class
31class EnclosureManagement {
32  constructor(private employeeName: string) {}
33
34  @LogOperation
35  openEnclosure(enclosureId: string): boolean {
36    console.log(`${this.employeeName} opens enclosure: ${enclosureId}`);
37    // Enclosure opening logic here
38    return true;
39  }
40
41  @LogOperation
42  closeEnclosure(enclosureId: string): boolean {
43    console.log(`${this.employeeName} closes enclosure: ${enclosureId}`);
44    // Enclosure closing logic here
45    return true;
46  }
47
48  checkStatus(enclosureId: string): string {
49    // This method is not logged
50    return "Enclosure closed";
51  }
52}
53
54// Using the class with decorated methods
55const manager = new EnclosureManagement("John");
56manager.openEnclosure("A-001");
57manager.closeEnclosure("A-001");
58manager.checkStatus("A-001"); // This operation will not be logged

Dans l'exemple ci-dessus, le décorateur LogOperation enregistre chaque appel de méthode, ses arguments et le résultat ou l'erreur. Les méthodes openEnclosure et closeEnclosure sont journalisées, tandis que la méthode checkStatus n'a pas de décorateur, elle n'est donc pas journalisée.

Décorateurs de méthode avec paramètres

On peut aussi créer des décorateurs de méthode qui acceptent des paramètres :

1// Method decorator with parameters
2function RequireAccessLevel(level: number) {
3  return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
4    // Save the original method
5    const originalMethod = descriptor.value;
6
7    // Override the method
8    descriptor.value = function(...args: any[]) {
9      // Check if the object has an access level property
10      if (!(this as any).accessLevel) {
11        throw new Error("Object does not have a defined access level!");
12      }
13
14      // Verify the access level
15      if ((this as any).accessLevel < level) {
16        throw new Error(`Required access level: ${level}, available: ${(this as any).accessLevel}`);
17      }
18
19      // Call the original method
20      return originalMethod.apply(this, args);
21    };
22
23    return descriptor;
24  };
25}
26
27// Class representing a laboratory
28class GeneticLaboratory {
29  constructor(public employeeName: string, public accessLevel: number) {}
30
31  @RequireAccessLevel(1)
32  readDnaData(dinosaurId: string): string {
33    return `Dinosaur DNA ${dinosaurId}: ACGT...`;
34  }
35
36  @RequireAccessLevel(2)
37  modifyDNA(dinosaurId: string, sequence: string): boolean {
38    console.log(`DNA modification of dinosaur ${dinosaurId}: ${sequence}`);
39    return true;
40  }
41
42  @RequireAccessLevel(3)
43  createNewSpecies(name: string, baseDNA: string): string {
44    const newSpeciesId = `NGT-${Math.floor(Math.random() * 1000)}`;
45    console.log(`Creating new species: ${name} (ID: ${newSpeciesId})`);
46    return newSpeciesId;
47  }
48}
49
50// Using the class with different access levels
51const intern = new GeneticLaboratory("Tim", 1);
52const scientist = new GeneticLaboratory("Dr. Grant", 2);
53const dyrektor = new GeneticLaboratory("Dr. Wu", 3);
54
55// Testing access
56try {
57  console.log(intern.readDnaData("D-001")); // Will work
58  intern.modifyDNA("D-001", "ACGT");         // Will throw error
59} catch (error) {
60  console.error(error.message);
61}
62
63try {
64  scientist.modifyDNA("D-001", "ACGT");        // Will work
65  scientist.createNewSpecies("Neo-Raptor", "ACGT..."); // Will throw error
66} catch (error) {
67  console.error(error.message);
68}
69
70// Director has full access
71dyrektor.createNewSpecies("Indominus Rex", "ACGT...");

Dans cet exemple, le décorateur RequireAccessLevel vérifie si la personne appelant la méthode dispose d'un niveau d'accès suffisant. Différentes méthodes requièrent différents niveaux d'accès, ce qui reflète les protocoles de sécurité réels qui pourraient exister à Jurassic Park.

Décorateurs de propriété

Les décorateurs de propriété sont similaires aux décorateurs de méthode mais ne reçoivent que deux paramètres :

  1. target - le prototype de la classe (pour les propriétés d'instance) ou le constructeur de la classe (pour les propriétés statiques)
  2. propertyKey - le nom de la propriété

Syntaxe de base d'un décorateur de propriété :

1function propertyDecorator(target: any, propertyKey: string) {
2  // Decorator implementation
3}
4
5class MyClass {
6  @propertyDecorator
7  myProperty: string;
8}

Comme les décorateurs de propriété ne reçoivent pas de descripteur, ils sont souvent utilisés pour générer des getters et des setters ou pour appliquer des métadonnées.

Exemple : système de surveillance des ressources à Jurassic Park

Implémentons un décorateur de propriété qui surveillera les changements de ressources à Jurassic Park :

1// Property decorator that monitors changes
2function MonitorChanges(target: any, propertyKey: string) {
3  // Create a unique name for the private property
4  const privateName = Symbol(propertyKey);
5
6  // Define getter and setter
7  Object.defineProperty(target, propertyKey, {
8    get: function() {
9      // Return the private property value
10      return this[privateName];
11    },
12    set: function(value: any) {
13      // Save the old value
14      const oldValue = this[privateName];
15
16      // Log the change if it's not the first assignment
17      if (this[privateName] !== undefined) {
18        console.log(`[CHANGE][${this.constructor.name}] ${propertyKey}: ${oldValue} -> ${value}`);
19      }
20
21      // Save the new value
22      this[privateName] = value;
23    },
24    enumerable: true,
25    configurable: true
26  });
27}
28
29// Class monitoring park resources
30class ParkResources {
31  @MonitorChanges
32  foodAmount: number = 1000;
33
34  @MonitorChanges
35  generatorsState: "Active" | "Backup" | "Disabled" = "Active";
36
37  @MonitorChanges
38  gateStatus: "Open" | "Closed" = "Closed";
39
40  constructor(public sectionName: string) {}
41
42  useFood(amount: number): boolean {
43    if (this.foodAmount >= amount) {
44      this.foodAmount -= amount;
45      return true;
46    }
47    return false;
48  }
49
50  refillFood(amount: number): void {
51    this.foodAmount += amount;
52  }
53
54  toggleGenerators(state: "Active" | "Backup" | "Disabled"): void {
55    this.generatorsState = state;
56  }
57
58  toggleGate(status: "Open" | "Closed"): void {
59    this.gateStatus = status;
60  }
61}
62
63// Using the class with monitored properties
64const sectionA = new ParkResources("Section A");
65
66// Testing property changes
67sectionA.useFood(200);  // foodAmount: 1000 -> 800
68sectionA.refillFood(500);  // foodAmount: 800 -> 1300
69sectionA.toggleGenerators("Backup");  // generatorsState: Active -> Backup
70sectionA.toggleGate("Open");  // gateStatus: Closed -> Open

Dans cet exemple, le décorateur MonitorChanges crée un getter et un setter pour les propriétés qui journalisent chaque changement de valeur. Ainsi, nous pouvons suivre quand et quels changements ont été effectués, ce qui est crucial pour surveiller les ressources du parc.

Combiner décorateurs de méthode et de propriété

On peut combiner différents décorateurs de méthode et de propriété pour créer des comportements plus avancés :

1// Decorator measuring method execution time
2function MeasureExecutionTime(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
3  const originalMethod = descriptor.value;
4
5  descriptor.value = function(...args: any[]) {
6    const start = performance.now();
7    const result = originalMethod.apply(this, args);
8    const end = performance.now();
9
10    console.log(`[TIME] ${propertyKey}: ${(end - start).toFixed(2)}ms`);
11
12    return result;
13  };
14
15  return descriptor;
16}
17
18// Decorator validating method parameters
19function ValidateParameters(validator: (args: any[]) => boolean, errorMessage: string) {
20  return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
21    const originalMethod = descriptor.value;
22
23    descriptor.value = function(...args: any[]) {
24      if (!validator(args)) {
25        throw new Error(`[VALIDATION ERROR] ${propertyKey}: ${errorMessage}`);
26      }
27
28      return originalMethod.apply(this, args);
29    };
30
31    return descriptor;
32  };
33}
34
35// Decorator caching method results
36function Cache(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
37  const originalMethod = descriptor.value;
38  const cache = new Map<string, any>();
39
40  descriptor.value = function(...args: any[]) {
41    const key = JSON.stringify(args);
42
43    if (cache.has(key)) {
44      console.log(`[CACHE] ${propertyKey}: Used cached result for ${key}`);
45      return cache.get(key);
46    }
47
48    const result = originalMethod.apply(this, args);
49    cache.set(key, result);
50
51    return result;
52  };
53
54  return descriptor;
55}
56
57// Decorator adding a value range for a numeric property
58function Range(min: number, max: number) {
59  return function(target: any, propertyKey: string) {
60    // Create a unique name for the private property
61    const privateName = Symbol(propertyKey);
62
63    // Define getter and setter
64    Object.defineProperty(target, propertyKey, {
65      get: function() {
66        return this[privateName];
67      },
68      set: function(value: number) {
69        if (value < min) {
70          console.warn(`[RANGE] ${propertyKey}: Value ${value} below minimum ${min}. Setting ${min}.`);
71          this[privateName] = min;
72        } else if (value > max) {
73          console.warn(`[RANGE] ${propertyKey}: Value ${value} above maximum ${max}. Setting ${max}.`);
74          this[privateName] = max;
75        } else {
76          this[privateName] = value;
77        }
78      },
79      enumerable: true,
80      configurable: true
81    });
82  };
83}
84
85// Class managing the electric fence system
86class ElectricFenceSystem {
87  @Range(0, 10000)
88  voltage: number = 5000; // in volts
89
90  @Range(0, 100)
91  range: number = 50; // in meters
92
93  private statusActive: boolean = false;
94
95  @LogOperation
96  @MeasureExecutionTime
97  @ValidateParameters(
98    args => args[0] >= 0 && args[0] <= 10000,
99    "Invalid voltage. Allowed range: 0-10000V"
100  )
101  setVoltage(newVoltage: number): boolean {
102    // Simulating a long operation for testing the MeasureExecutionTime decorator
103    for (let i = 0; i < 1000000; i++) {
104      // CPU load
105    }
106
107    this.voltage = newVoltage;
108    return true;
109  }
110
111  @LogOperation
112  @Cache
113  getSystemStatus(sectionId: string): { active: boolean; voltage: number; range: number } {
114    console.log(`Fetching system status for section ${sectionId}...`);
115
116    // Simulating a long operation
117    for (let i = 0; i < 500000; i++) {
118      // CPU load
119    }
120
121    return {
122      active: this.statusActive,
123      voltage: this.voltage,
124      range: this.range
125    };
126  }
127
128  @LogOperation
129  @MeasureExecutionTime
130  activateSystem(): boolean {
131    if (this.statusActive) {
132      console.log("System already active");
133      return false;
134    }
135
136    console.log(`Activating electric fence system. Voltage: ${this.voltage}V, Range: ${this.range}m`);
137    this.statusActive = true;
138
139    return true;
140  }
141
142  @LogOperation
143  @MeasureExecutionTime
144  deactivateSystem(): boolean {
145    if (!this.statusActive) {
146      console.log("System already inactive");
147      return false;
148    }
149
150    console.log("Deactivating electric fence system");
151    this.statusActive = false;
152
153    return true;
154  }
155}
156
157// Using the class with multiple decorators
158const fenceSystem = new ElectricFenceSystem();
159
160// Testing various functionalities
161fenceSystem.setVoltage(7500);  // Logs operation, measures time, validates parameter
162fenceSystem.setVoltage(12000); // Will exceed range, will be set to 10000
163fenceSystem.activateSystem();      // Logs operation, measures time
164
165// Testing caching
166console.log(fenceSystem.getSystemStatus("A1")); // Full operation
167console.log(fenceSystem.getSystemStatus("A1")); // Uses cache
168console.log(fenceSystem.getSystemStatus("B2")); // Full operation
169console.log(fenceSystem.getSystemStatus("B2")); // Uses cache
170
171// Deactivating the system
172fenceSystem.deactivateSystem();

Dans cet exemple, nous utilisons plusieurs décorateurs pour créer un système avancé de clôture électrique :

  1. @Range - garantit que les valeurs de la propriété restent dans la plage indiquée
  2. @LogOperation - journalise les appels de méthode
  3. @MeasureExecutionTime - mesure le temps d'exécution des méthodes
  4. @ValidateParameters - vérifie la validité des paramètres des méthodes
  5. @Cache - met en cache les résultats des méthodes en mémoire

Métadonnées et réflexion avec les décorateurs

TypeScript permet aussi d'utiliser des métadonnées avec les décorateurs, ce qui ouvre la voie à des cas d'usage plus avancés. Cela nécessite d'activer les options emitDecoratorMetadata et experimentalDecorators dans le fichier tsconfig.json et d'installer le paquet reflect-metadata.

Voici un exemple d'utilisation des métadonnées avec les décorateurs :

1// We need to import reflect-metadata at the start of the application
2import "reflect-metadata";
3
4// Keys for metadata
5const PARAMETER_KEY = Symbol("parameters");
6
7// Parameter decorator
8function Parameter(target: any, propertyKey: string, parameterIndex: number) {
9  // Get existing metadata or create empty
10  const parameters: number[] = Reflect.getOwnMetadata(PARAMETER_KEY, target, propertyKey) || [];
11
12  // Add parameter index to the list
13  parameters.push(parameterIndex);
14
15  // Save metadata
16  Reflect.defineMetadata(PARAMETER_KEY, parameters, target, propertyKey);
17}
18
19// Method decorator validating parameters
20function ValidateData(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
21  const originalMethod = descriptor.value;
22
23  descriptor.value = function(...args: any[]) {
24    // Get parameter types (requires emitDecoratorMetadata option)
25    const types = Reflect.getMetadata("design:paramtypes", target, propertyKey);
26
27    // Get parameter indices marked with the @Parameter decorator
28    const markedParameters: number[] = Reflect.getOwnMetadata(PARAMETER_KEY, target, propertyKey) || [];
29
30    // Validate parameters
31    for (const index of markedParameters) {
32      const value = args[index];
33      const type = types[index];
34
35      // Type validation
36      if (value === null || value === undefined) {
37        throw new Error(`Parameter #${index} of method ${propertyKey} cannot be null or undefined`);
38      }
39
40      // Validation for different types
41      if (type === String && typeof value !== "string") {
42        throw new Error(`Parameter #${index} of method ${propertyKey} must be a string`);
43      } else if (type === Number && typeof value !== "number") {
44        throw new Error(`Parameter #${index} of method ${propertyKey} must be a number`);
45      } else if (type === Boolean && typeof value !== "boolean") {
46        throw new Error(`Parameter #${index} of method ${propertyKey} must be a boolean`);
47      } else if (type === Date && !(value instanceof Date)) {
48        throw new Error(`Parameter #${index} of method ${propertyKey} must be a Date`);
49      }
50    }
51
52    // Call the original method
53    return originalMethod.apply(this, args);
54  };
55
56  return descriptor;
57}
58
59// Dinosaur operations class with metadata
60class DinosaurOperations {
61  @ValidateData
62  conductResearchHealth(
63    dinosaurId: string,
64    @Parameter examinationDate: Date,
65    @Parameter temperature: number,
66    @Parameter weight: number,
67    notes?: string
68  ): string {
69    console.log(`Health examination of dinosaur ${dinosaurId}`);
70    console.log(`Date: ${examinationDate.toLocaleDateString()}`);
71    console.log(`Temperature: ${temperature}°C`);
72    console.log(`Weight: ${weight}kg`);
73
74    if (notes) {
75      console.log(`Notes: ${notes}`);
76    }
77
78    return "Examination completed";
79  }
80
81  @ValidateData
82  performVaccination(
83    @Parameter dinosaurId: string,
84    @Parameter vaccinationType: string,
85    @Parameter vaccinationDate: Date
86  ): boolean {
87    console.log(`Vaccination of dinosaur ${dinosaurId}`);
88    console.log(`Vaccination type: ${vaccinationType}`);
89    console.log(`Date: ${vaccinationDate.toLocaleDateString()}`);
90
91    return true;
92  }
93}
94
95// Using the class with parameter validation
96const operations = new DinosaurOperations();
97
98try {
99  // Correct call
100  operations.conductResearchHealth(
101    "D-001",
102    new Date(),
103    38.5,
104    1200,
105    "Dinosaur in good condition"
106  );
107
108  // Incorrect call - temperature is not a number
109  operations.conductResearchHealth(
110    "D-002",
111    new Date(),
112    "high" as any, // This will cause an error
113    1500
114  );
115} catch (error) {
116  console.error(error.message);
117}
118
119try {
120  // Correct call
121  operations.performVaccination(
122    "D-003",
123    "Anti-parasite vaccine",
124    new Date()
125  );
126
127  // Incorrect call - date is not a Date object
128  operations.performVaccination(
129    "D-004",
130    "Anti-flu vaccine",
131    "today" as any // This will cause an error
132  );
133} catch (error) {
134  console.error(error.message);
135}

Dans cet exemple, nous utilisons les métadonnées et la réflexion pour valider les paramètres d'une méthode. Le décorateur @Parameter marque les paramètres qui doivent être validés, et le décorateur @ValidateData vérifie les types de ces paramètres à chaque appel de la méthode.

Exemple pratique : système de gestion des examens de dinosaures

Combinons maintenant toutes les techniques abordées dans un exemple plus vaste - un système de gestion des examens de dinosaures :

1// Property decorator ensuring unique IDs
2function UniqueID(prefix: string = "ID") {
3  return function(target: any, propertyKey: string) {
4    const generateId = () => `${prefix}-${Date.now()}-${Math.floor(Math.random() * 1000)}`;
5
6    Object.defineProperty(target, propertyKey, {
7      get: function() {
8        // If the property hasn't been set yet, generate an ID
9        if (!this[`_${propertyKey}`]) {
10          this[`_${propertyKey}`] = generateId();
11        }
12        return this[`_${propertyKey}`];
13      },
14      set: function(value: string) {
15        // Allow setting only once
16        if (!this[`_${propertyKey}`]) {
17          this[`_${propertyKey}`] = value;
18        } else {
19          console.warn(`Property ${propertyKey} has already been set and cannot be changed.`);
20        }
21      },
22      enumerable: true,
23      configurable: true
24    });
25  };
26}
27
28// Method decorator for date validation
29function ValidateExaminationDate(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
30  const originalMethod = descriptor.value;
31
32  descriptor.value = function(...args: any[]) {
33    // Assume the first argument is the examination date
34    const examinationDate = args[0];
35
36    if (!(examinationDate instanceof Date)) {
37      throw new Error("First parameter must be a Date instance");
38    }
39
40    const now = new Date();
41
42    if (examinationDate > now) {
43      throw new Error("Examination date cannot be in the future");
44    }
45
46    // Check if the date is not older than 30 days
47    const thirtyDaysAgo = new Date();
48    thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
49
50    if (examinationDate < thirtyDaysAgo) {
51      console.warn("Warning: Examination date is older than 30 days");
52    }
53
54    return originalMethod.apply(this, args);
55  };
56
57  return descriptor;
58}
59
60// Method decorator for vaccination frequency control
61function ControlFrequency(minDaysBetweenVaccinations: number) {
62  return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
63    // Map storing last vaccination dates for each dinosaur
64    const lastVaccinations = new Map<string, Date>();
65
66    const originalMethod = descriptor.value;
67
68    descriptor.value = function(...args: any[]) {
69      // Assume first argument is dinosaur ID, second is vaccination date
70      const dinosaurId = args[0];
71      const vaccinationDate = args[1];
72
73      if (lastVaccinations.has(dinosaurId)) {
74        const lastVaccine = lastVaccinations.get(dinosaurId)!;
75        const differenceDays = Math.floor((vaccinationDate.getTime() - lastVaccine.getTime()) / (1000 * 60 * 60 * 24));
76
77        if (differenceDays < minDaysBetweenVaccinations) {
78          throw new Error(`Too early for next vaccination. Minimum interval is ${minDaysBetweenVaccinations} days. ${minDaysBetweenVaccinations - differenceDays} days remaining.`);
79        }
80      }
81
82      // Save vaccination date
83      lastVaccinations.set(dinosaurId, vaccinationDate);
84
85      return originalMethod.apply(this, args);
86    };
87
88    return descriptor;
89  };
90}
91
92// Examination results interface
93interface ExaminationResults {
94  temperature: number;  // in degrees Celsius
95  weight: number;         // in kilograms
96  length: number;      // in meters
97  healthStatus: "Healthy" | "Light symptoms" | "Sick" | "Critical";
98  notes?: string;
99}
100
101// Dinosaur examination class
102class DinosaurExamination {
103  @UniqueID("BAD")
104  examinationId: string;
105
106  constructor(
107    public dinosaurId: string,
108    public examinationDate: Date,
109    public results: ExaminationResults,
110    public examiner: string
111  ) {}
112
113  @LogOperation
114  getReport(): string {
115    return `
116      === EXAMINATION REPORT ===
117      Examination ID: ${this.examinationId}
118      Dinosaur ID: ${this.dinosaurId}
119      Date: ${this.examinationDate.toLocaleString()}
120      Conducted by: ${this.examiner}
121
122      RESULTS:
123      - Temperature: ${this.results.temperature}°C
124      - Weight: ${this.results.weight} kg
125      - Length: ${this.results.length} m
126      - Health status: ${this.results.healthStatus}
127      ${this.results.notes ? `- Notes: ${this.results.notes}` : ""}
128    `;
129  }
130}
131
132// Dinosaur vaccination class
133class DinosaurVaccination {
134  @UniqueID("SZCZ")
135  vaccinationId: string;
136
137  constructor(
138    public dinosaurId: string,
139    public vaccinationDate: Date,
140    public genusVaccine: string,
141    public dose: number,
142    public vaccinator: string
143  ) {}
144
145  @LogOperation
146  getReport(): string {
147    return `
148      === VACCINATION REPORT ===
149      Vaccination ID: ${this.vaccinationId}
150      Dinosaur ID: ${this.dinosaurId}
151      Date: ${this.vaccinationDate.toLocaleString()}
152      Vaccine type: ${this.genusVaccine}
153      Dose: ${this.dose} ml
154      Conducted by: ${this.vaccinator}
155    `;
156  }
157}
158
159// Examination and vaccination registry
160class MedicalRegistry {
161  private examinations: DinosaurExamination[] = [];
162  private vaccinations: DinosaurVaccination[] = [];
163
164  @LogOperation
165  @ValidateExaminationDate
166  registerExamination(
167    examinationDate: Date,
168    dinosaurId: string,
169    temperature: number,
170    weight: number,
171    length: number,
172    healthStatus: "Healthy" | "Light symptoms" | "Sick" | "Critical",
173    notes?: string,
174    examiner: string = "Dr. Harding"
175  ): DinosaurExamination {
176    const results: ExaminationResults = {
177      temperature,
178      weight,
179      length,
180      healthStatus,
181      notes
182    };
183
184    const examination = new DinosaurExamination(dinosaurId, examinationDate, results, examiner);
185    this.examinations.push(examination);
186
187    console.log(`Examination registered: ${examination.examinationId} for dinosaur ${dinosaurId}`);
188
189    return examination;
190  }
191
192  @LogOperation
193  @ControlFrequency(90) // Minimum 90 days between vaccinations
194  registerVaccination(
195    dinosaurId: string,
196    vaccinationDate: Date,
197    genusVaccine: string,
198    dose: number,
199    vaccinator: string = "Dr. Harding"
200  ): DinosaurVaccination {
201    const vaccination = new DinosaurVaccination(
202      dinosaurId,
203      vaccinationDate,
204      genusVaccine,
205      dose,
206      vaccinator
207    );
208
209    this.vaccinations.push(vaccination);
210
211    console.log(`Vaccination registered: ${vaccination.vaccinationId} for dinosaur ${dinosaurId}`);
212
213    return vaccination;
214  }
215
216  @Cache
217  @LogOperation
218  getExaminationHistory(dinosaurId: string): DinosaurExamination[] {
219    console.log(`Fetching examination history for dinosaur ${dinosaurId}...`);
220
221    // Simulating a long operation
222    for (let i = 0; i < 500000; i++) {
223      // CPU load
224    }
225
226    return this.examinations.filter(examination => examination.dinosaurId === dinosaurId)
227      .sort((a, b) => b.examinationDate.getTime() - a.examinationDate.getTime());
228  }
229
230  @Cache
231  @LogOperation
232  getVaccinationHistory(dinosaurId: string): DinosaurVaccination[] {
233    console.log(`Fetching vaccination history for dinosaur ${dinosaurId}...`);
234
235    // Simulating a long operation
236    for (let i = 0; i < 500000; i++) {
237      // CPU load
238    }
239
240    return this.vaccinations.filter(vaccination => vaccination.dinosaurId === dinosaurId)
241      .sort((a, b) => b.vaccinationDate.getTime() - a.vaccinationDate.getTime());
242  }
243
244  @MeasureExecutionTime
245  @LogOperation
246  generateMedicalReport(dinosaurId: string): string {
247    const examinations = this.getExaminationHistory(dinosaurId);
248    const vaccinations = this.getVaccinationHistory(dinosaurId);
249
250    let report = `=== MEDICAL REPORT FOR DINOSAUR ${dinosaurId} ===\n\n`;
251
252    report += `Number of examinations: ${examinations.length}\n`;
253    report += `Number of vaccinations: ${vaccinations.length}\n\n`;
254
255    if (examinations.length > 0) {
256      const lastExamination = examinations[0];
257      report += `LAST EXAMINATION:\n`;
258      report += `Date: ${lastExamination.examinationDate.toLocaleString()}\n`;
259      report += `Health status: ${lastExamination.results.healthStatus}\n`;
260      report += `Temperature: ${lastExamination.results.temperature}°C\n`;
261      report += `Weight: ${lastExamination.results.weight} kg\n`;
262      report += `Length: ${lastExamination.results.length} m\n`;
263      if (lastExamination.results.notes) {
264        report += `Notes: ${lastExamination.results.notes}\n`;
265      }
266      report += "\n";
267    }
268
269    if (vaccinations.length > 0) {
270      const lastVaccination = vaccinations[0];
271      report += `LAST VACCINATION:\n`;
272      report += `Date: ${lastVaccination.vaccinationDate.toLocaleString()}\n`;
273      report += `Vaccine type: ${lastVaccination.genusVaccine}\n`;
274      report += `Dose: ${lastVaccination.dose} ml\n\n`;
275    }
276
277    report += `EXAMINATION HISTORY:\n`;
278    examinations.forEach((examination, index) => {
279      report += `${index + 1}. ${examination.examinationDate.toLocaleDateString()} - ${examination.results.healthStatus}\n`;
280    });
281
282    report += `\nVACCINATION HISTORY:\n`;
283    vaccinations.forEach((vaccination, index) => {
284      report += `${index + 1}. ${vaccination.vaccinationDate.toLocaleDateString()} - ${vaccination.genusVaccine}\n`;
285    });
286
287    return report;
288  }
289}
290
291// Testing the system
292const medicalRegistry = new MedicalRegistry();
293
294// Registering examinations and vaccinations
295try {
296  // Past examination - OK
297  const examination1 = medicalRegistry.registerExamination(
298    new Date("2023-09-01"),
299    "TREX-001",
300    38.2,
301    7500,
302    12.5,
303    "Healthy",
304    "Routine examination",
305    "Dr. Harding"
306  );
307
308  // Future examination - Error
309  const examination2 = medicalRegistry.registerExamination(
310    new Date("2025-01-01"), // future
311    "TREX-001",
312    39.1,
313    7450,
314    12.5,
315    "Light symptoms",
316    "Slight fever"
317  );
318} catch (error) {
319  console.error(error.message);
320}
321
322// Registering vaccinations
323try {
324  // First vaccination
325  const vaccination1 = medicalRegistry.registerVaccination(
326    "TREX-001",
327    new Date("2023-01-15"),
328    "Dinosaur flu vaccine",
329    50,
330    "Dr. Sattler"
331  );
332
333  // Second vaccination - too early, should throw error
334  const vaccination2 = medicalRegistry.registerVaccination(
335    "TREX-001",
336    new Date("2023-03-15"), // Only 60 days later, minimum is 90 days
337    "Anti-parasite vaccine",
338    30
339  );
340} catch (error) {
341  console.error(error.message);
342}
343
344// Testing data retrieval
345console.log(medicalRegistry.getExaminationHistory("TREX-001")); // Full fetch
346console.log(medicalRegistry.getExaminationHistory("TREX-001")); // Uses cache
347
348// Generating medical report
349console.log(medicalRegistry.generateMedicalReport("TREX-001"));

Dans cet exemple élaboré, nous avons créé un système complet de gestion des examens et des vaccinations des dinosaures à Jurassic Park, en utilisant divers décorateurs :

  1. @UniqueID - garantit que chaque examen et chaque vaccination a un identifiant unique
  2. @LogOperation - journalise les appels de méthode
  3. @ValidateExaminationDate - valide l'exactitude de la date d'examen
  4. @ControlFrequency - garantit des intervalles appropriés entre les vaccinations
  5. @Cache - met en cache les résultats des opérations longues
  6. @MeasureExecutionTime - mesure le temps de génération des rapports

Ce système montre comment les décorateurs de méthode et de propriété peuvent être utilisés pour créer des fonctionnalités métier avancées tout en garantissant la cohérence du code, la sécurité des données et la performance.

Quand utiliser les décorateurs de méthode et de propriété

Les décorateurs de méthode et de propriété sont particulièrement utiles :

  1. Lorsque tu veux ajouter une fonctionnalité orientée aspect - journalisation, mesure de performance, validation
  2. Lorsque tu veux mettre en cache des résultats - pour des méthodes coûteuses en calcul
  3. Lorsque tu as besoin de valider des paramètres - vérification des types, des plages, de l'exactitude
  4. Lorsque tu veux contrôler l'accès - autorisation, vérification des permissions
  5. Lorsque tu veux ajouter des métadonnées - pour des frameworks, des ORM, la sérialisation
  6. Lorsque tu dois surveiller des changements - suivi des modifications de propriétés

Limites et problèmes

Comme les décorateurs de classe, les décorateurs de méthode et de propriété ont leurs limites et leurs problèmes :

  1. Débogage plus difficile - un code comportant de nombreux décorateurs peut être plus difficile à suivre
  2. Problèmes de typage - peut nécessiter des conversions de type (par ex. à l'aide de as any)
  3. Logique cachée - l'implémentation peut être masquée dans le décorateur, rendant le code plus difficile à comprendre
  4. Ordre d'application - l'ordre des décorateurs a son importance et peut provoquer des bugs subtils
  5. Évolutions de la spécification - la spécification des décorateurs continue d'évoluer en JavaScript et TypeScript

Résumé

Les décorateurs de méthode et de propriété en TypeScript sont des outils puissants qui permettent d'ajouter des fonctionnalités supplémentaires aux méthodes et aux propriétés d'une classe de manière déclarative. Tout comme les protocoles opérationnels de Jurassic Park aident à contrôler les actions individuelles, les décorateurs de méthode et de propriété aident à contrôler et à étendre le comportement des éléments individuels d'une classe.

Grâce aux décorateurs, nous pouvons créer des applications plus modulaires, lisibles et faciles à maintenir, en isolant les fonctionnalités transversales telles que la journalisation, la validation ou la sécurité. Dans les grands projets, des décorateurs bien conçus peuvent améliorer significativement la qualité du code et réduire la redondance.

Cependant, comme tout outil avancé, les décorateurs de méthode et de propriété doivent être utilisés avec discernement. Un usage excessif des décorateurs peut conduire à un code difficile à comprendre, à déboguer et à tester. La meilleure pratique consiste à les utiliser là où ils apportent réellement des bénéfices, et non simplement pour le plaisir de les utiliser.

Code de cette leçon : index.ts
1// Décorateurs de méthodes et de propriétés - Supervision des opérations du Parc Jurassique
2// Note: Les décorateurs nécessitent "experimentalDecorators": true dans tsconfig.json
3
4// === DÉCORATEUR DE MÉTHODE - Journalisation des opérations ===
5function LogOperation(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
6  const originalMethod = descriptor.value;
7
8  descriptor.value = function (...args: any[]) {
9    console.log("[LOG] Appel: " + propertyKey + "(" + args.join(", ") + ")");
10    const startTime = Date.now();
11    const result = originalMethod.apply(this, args);
12    const duration = Date.now() - startTime;
13    console.log("[LOG] Terminé: " + propertyKey + " (" + duration + "ms)");
14    return result;
15  };
16
17  return descriptor;
18}
19
20// === DÉCORATEUR DE MÉTHODE - Validation des paramètres ===
21function ValidateSector(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
22  const originalMethod = descriptor.value;
23
24  descriptor.value = function (...args: any[]) {
25    const sectorId = args[0];
26    if (typeof sectorId !== "string" || !/^[A-Z]\d{1,2}$/.test(sectorId)) {
27      throw new Error("Secteur invalide: " + sectorId);
28    }
29    return originalMethod.apply(this, args);
30  };
31
32  return descriptor;
33}
34
35// === DÉCORATEUR DE MÉTHODE - Mémoïsation (cache) ===
36function Memoize(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
37  const originalMethod = descriptor.value;
38  const cache = new Map<string, any>();
39
40  descriptor.value = function (...args: any[]) {
41    const key = JSON.stringify(args);
42    if (cache.has(key)) {
43      console.log("[CACHE] Résultat en cache pour " + propertyKey);
44      return cache.get(key);
45    }
46    const result = originalMethod.apply(this, args);
47    cache.set(key, result);
48    return result;
49  };
50
51  return descriptor;
52}
53
54// === CLASSE AVEC DÉCORATEURS ===
55class DinosaurTracker {
56  private dinosaurs: Map<string, any> = new Map();
57
58  constructor() {
59    this.dinosaurs.set("A1", { name: "Rex", species: "T-Rex", health: 95 });
60    this.dinosaurs.set("B3", { name: "Blue", species: "Velociraptor", health: 88 });
61    this.dinosaurs.set("C2", { name: "Bronty", species: "Brachiosaurus", health: 100 });
62  }
63
64  @LogOperation
65  @ValidateSector
66  checkSector(sectorId: string): any {
67    const dino = this.dinosaurs.get(sectorId);
68    if (!dino) return { sector: sectorId, status: "vide" };
69    return { sector: sectorId, ...dino, status: "actif" };
70  }
71
72  @LogOperation
73  @Memoize
74  calculateFeedingCost(species: string, days: number): number {
75    // Simulation de calculs coûteux
76    const costs: Record<string, number> = {
77      "T-Rex": 500,
78      "Velociraptor": 200,
79      "Brachiosaurus": 300,
80    };
81    return (costs[species] || 100) * days;
82  }
83}
84
85// === TESTS ===
86const tracker = new DinosaurTracker();
87
88console.log("=== Vérification des secteurs ===");
89console.log(tracker.checkSector("A1"));
90console.log(tracker.checkSector("B3"));
91
92console.log("\n=== Coûts de nourrissage ===");
93console.log("T-Rex 7 jours:", tracker.calculateFeedingCost("T-Rex", 7));
94console.log("T-Rex 7 jours (cache):", tracker.calculateFeedingCost("T-Rex", 7));
95
96console.log("\n=== Validation du secteur ===");
97try {
98  tracker.checkSector("invalid");
99} catch (e: any) {
100  console.error("Erreur:", e.message);
101}
102
103// TODO : Crée un décorateur @ReadOnly qui empêche de redéfinir la méthode
104// TODO : Crée un décorateur @Throttle(ms) limitant la fréquence des appels

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 décorateur en TypeScript reçoit le constructeur de la classe comme argument et peut l'étendre ou le remplacer ?

  2. 2. Quels trois arguments un décorateur de méthode reçoit-il en TypeScript ?

Tâches pratiques dans le jeu

  • Éditeur de code

    Implémente les décorateurs Trackable (classe), Authorize (méthode) et MaxLength (propriété) selon les instructions dans le code.

  • Tri vertical

    Ordonne les étapes de création d'un décorateur de méthode en TypeScript :

Articles utiles