We use cookies to enhance your experience on the site
CodeWorlds

Method and Property Decorators

"Oversight of individual operations in Jurassic Park is just as important as managing the entire park," explains Dr. Henry Wu, InGen's chief geneticist. "Just as we monitor every aspect of dinosaur life - their feeding, behavior, and growth - we must also control and secure individual operations in our system."

In TypeScript, after understanding class decorators, the natural next step is to delve into method and property decorators. These decorators allow us to precisely modify the behavior of specific class members, just as in Jurassic Park, where every operation, from feeding dinosaurs to securing enclosures, requires special oversight and protocols.

Introduction to method and property decorators

Method and property decorators allow modifying the behavior, access, or value of individual class members. Thanks to them, we can:

  1. Add additional behavior before or after method execution
  2. Validate method parameters
  3. Cache method results
  4. Measure method execution time
  5. Control property access
  6. Transform property values
  7. Add metadata to methods and properties

Method decorators

Method decorators are functions that take three parameters:

  1. target
    - the class prototype (for instance methods) or the class constructor (for static methods)
  2. propertyKey
    - the method name
  3. descriptor
    - the property descriptor for the method

Basic method decorator syntax:

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

Example: Operations logging system in Jurassic Park

Let's start with a simple method decorator that will log operation execution in Jurassic Park:

1// Method decorator that logs calls
2function LogOperation(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
3  // Save the original method
4  const oryginalnaMetoda = 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 wynik = oryginalnaMetoda.apply(this, args);
13
14      // Log the result
15      console.log(`[${new Date().toISOString()}] ${propertyKey} completed. Result: ${JSON.stringify(wynik)}`);
16
17      return wynik;
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 nazwaPracownika: string) {}
33
34  @LogOperation
35  openEnclosure(idZagrody: string): boolean {
36    console.log(`${this.nazwaPracownika} opens enclosure: ${idZagrody}`);
37    // Enclosure opening logic here
38    return true;
39  }
40
41  @LogOperation
42  closeEnclosure(idZagrody: string): boolean {
43    console.log(`${this.nazwaPracownika} closes enclosure: ${idZagrody}`);
44    // Enclosure closing logic here
45    return true;
46  }
47
48  checkStatus(idZagrody: 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

In the example above, the

LogOperation
decorator records every method call, its arguments, and the result or error. The
openEnclosure
and
closeEnclosure
methods are logged, while the
checkStatus
method has no decorator, so it is not logged.

Method decorators with parameters

We can also create method decorators that accept parameters:

1// Method decorator with parameters
2function RequireAccessLevel(poziom: number) {
3  return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
4    // Save the original method
5    const oryginalnaMetoda = 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 < poziom) {
16        throw new Error(`Required access level: ${poziom}, available: ${(this as any).accessLevel}`);
17      }
18
19      // Call the original method
20      return oryginalnaMetoda.apply(this, args);
21    };
22
23    return descriptor;
24  };
25}
26
27// Class representing a laboratory
28class LaboratoriumGenetyczne {
29  constructor(public nazwaPracownika: string, public accessLevel: number) {}
30
31  @RequireAccessLevel(1)
32  odczytajDaneDNA(idDinosaura: string): string {
33    return `Dinosaur DNA ${idDinosaura}: ACGT...`;
34  }
35
36  @RequireAccessLevel(2)
37  modyfikujDNA(idDinosaura: string, sequence: string): boolean {
38    console.log(`DNA modification of dinosaur ${idDinosaura}: ${sequence}`);
39    return true;
40  }
41
42  @RequireAccessLevel(3)
43  createNewSpecies(nazwa: string, bazoweDNA: string): string {
44    const nowySpeciesId = `NGT-${Math.floor(Math.random() * 1000)}`;
45    console.log(`Creating new species: ${nazwa} (ID: ${nowySpeciesId})`);
46    return nowySpeciesId;
47  }
48}
49
50// Using the class with different access levels
51const intern = new LaboratoriumGenetyczne("Tim", 1);
52const scientist = new LaboratoriumGenetyczne("Dr. Grant", 2);
53const dyrektor = new LaboratoriumGenetyczne("Dr. Wu", 3);
54
55// Testing access
56try {
57  console.log(intern.odczytajDaneDNA("D-001")); // Will work
58  intern.modyfikujDNA("D-001", "ACGT");         // Will throw error
59} catch (error) {
60  console.error(error.message);
61}
62
63try {
64  scientist.modyfikujDNA("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...");

In this example, the

RequireAccessLevel
decorator checks whether the person calling the method has a sufficient access level. Different methods require different access levels, reflecting real security protocols that could exist in Jurassic Park.

Property decorators

Property decorators are similar to method decorators but receive only two parameters:

  1. target
    - the class prototype (for instance properties) or the class constructor (for static properties)
  2. propertyKey
    - the property name

Basic property decorator syntax:

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

Since property decorators don't receive a descriptor, they are often used to generate getters and setters or to apply metadata.

Example: Resource monitoring system in Jurassic Park

Let's implement a property decorator that will monitor resource changes in Jurassic Park:

1// Property decorator that monitors changes
2function MonitorujZmiany(target: any, propertyKey: string) {
3  // Create a unique name for the private property
4  const prywatnaName = 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[prywatnaName];
11    },
12    set: function(value: any) {
13      // Save the old value
14      const oldValue = this[prywatnaName];
15
16      // Log the change if it's not the first assignment
17      if (this[prywatnaName] !== undefined) {
18        console.log(`[CHANGE][${this.constructor.name}] ${propertyKey}: ${oldValue} -> ${value}`);
19      }
20
21      // Save the new value
22      this[prywatnaName] = value;
23    },
24    enumerable: true,
25    configurable: true
26  });
27}
28
29// Class monitoring park resources
30class ZasobyParku {
31  @MonitorujZmiany
32  foodAmount: number = 1000;
33
34  @MonitorujZmiany
35  generatorsState: "Aktywne" | "Awaryjne" | "Disabled" = "Aktywne";
36
37  @MonitorujZmiany
38  statusBramy: "Otwarta" | "Closed" = "Closed";
39
40  constructor(public nazwaSekcji: 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(stan: "Aktywne" | "Awaryjne" | "Disabled"): void {
55    this.generatorsState = stan;
56  }
57
58  toggleGate(status: "Otwarta" | "Closed"): void {
59    this.statusBramy = status;
60  }
61}
62
63// Using the class with monitored properties
64const sekcjaA = new ZasobyParku("Section A");
65
66// Testing property changes
67sekcjaA.useFood(200);  // foodAmount: 1000 -> 800
68sekcjaA.refillFood(500);  // foodAmount: 800 -> 1300
69sekcjaA.toggleGenerators("Awaryjne");  // generatorsState: Aktywne -> Awaryjne
70sekcjaA.toggleGate("Otwarta");  // statusBramy: Closed -> Otwarta

In this example, the

MonitorujZmiany
decorator creates a getter and setter for properties that log every value change. This way, we can track when and what changes were made, which is crucial for monitoring park resources.

Combining method and property decorators

We can combine different method and property decorators to create more advanced behaviors:

1// Decorator measuring method execution time
2function MierzCzasWykonania(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
3  const oryginalnaMetoda = descriptor.value;
4
5  descriptor.value = function(...args: any[]) {
6    const start = performance.now();
7    const wynik = oryginalnaMetoda.apply(this, args);
8    const koniec = performance.now();
9
10    console.log(`[TIME] ${propertyKey}: ${(koniec - start).toFixed(2)}ms`);
11
12    return wynik;
13  };
14
15  return descriptor;
16}
17
18// Decorator validating method parameters
19function WalidujParametry(walidator: (args: any[]) => boolean, errorMessage: string) {
20  return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
21    const oryginalnaMetoda = descriptor.value;
22
23    descriptor.value = function(...args: any[]) {
24      if (!walidator(args)) {
25        throw new Error(`[VALIDATION ERROR] ${propertyKey}: ${errorMessage}`);
26      }
27
28      return oryginalnaMetoda.apply(this, args);
29    };
30
31    return descriptor;
32  };
33}
34
35// Decorator caching method results
36function Cachuj(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
37  const oryginalnaMetoda = descriptor.value;
38  const cache = new Map<string, any>();
39
40  descriptor.value = function(...args: any[]) {
41    const klucz = JSON.stringify(args);
42
43    if (cache.has(klucz)) {
44      console.log(`[CACHE] ${propertyKey}: Used cached result for ${klucz}`);
45      return cache.get(klucz);
46    }
47
48    const wynik = oryginalnaMetoda.apply(this, args);
49    cache.set(klucz, wynik);
50
51    return wynik;
52  };
53
54  return descriptor;
55}
56
57// Decorator adding a value range for a numeric property
58function Zakres(min: number, max: number) {
59  return function(target: any, propertyKey: string) {
60    // Create a unique name for the private property
61    const prywatnaName = Symbol(propertyKey);
62
63    // Define getter and setter
64    Object.defineProperty(target, propertyKey, {
65      get: function() {
66        return this[prywatnaName];
67      },
68      set: function(value: number) {
69        if (value < min) {
70          console.warn(`[RANGE] ${propertyKey}: Value ${value} below minimum ${min}. Setting ${min}.`);
71          this[prywatnaName] = min;
72        } else if (value > max) {
73          console.warn(`[RANGE] ${propertyKey}: Value ${value} above maximum ${max}. Setting ${max}.`);
74          this[prywatnaName] = max;
75        } else {
76          this[prywatnaName] = value;
77        }
78      },
79      enumerable: true,
80      configurable: true
81    });
82  };
83}
84
85// Class managing the electric fence system
86class ElectricFenceSystem {
87  @Zakres(0, 10000)
88  voltage: number = 5000; // in volts
89
90  @Zakres(0, 100)
91  range: number = 50; // in meters
92
93  private statusActive: boolean = false;
94
95  @LogOperation
96  @MierzCzasWykonania
97  @WalidujParametry(
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 MierzCzasWykonania 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  @Cachuj
113  pobierzStatusSystemu(idSekcji: string): { aktywny: boolean; voltage: number; range: number } {
114    console.log(`Fetching system status for section ${idSekcji}...`);
115
116    // Simulating a long operation
117    for (let i = 0; i < 500000; i++) {
118      // CPU load
119    }
120
121    return {
122      aktywny: this.statusActive,
123      voltage: this.voltage,
124      range: this.range
125    };
126  }
127
128  @LogOperation
129  @MierzCzasWykonania
130  aktywujSystem(): 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  @MierzCzasWykonania
144  deaktywujSystem(): 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 systemOgrodzenia = new ElectricFenceSystem();
159
160// Testing various functionalities
161systemOgrodzenia.setVoltage(7500);  // Logs operation, measures time, validates parameter
162systemOgrodzenia.setVoltage(12000); // Will exceed range, will be set to 10000
163systemOgrodzenia.aktywujSystem();      // Logs operation, measures time
164
165// Testing caching
166console.log(systemOgrodzenia.pobierzStatusSystemu("A1")); // Full operation
167console.log(systemOgrodzenia.pobierzStatusSystemu("A1")); // Uses cache
168console.log(systemOgrodzenia.pobierzStatusSystemu("B2")); // Full operation
169console.log(systemOgrodzenia.pobierzStatusSystemu("B2")); // Uses cache
170
171// Deactivating the system
172systemOgrodzenia.deaktywujSystem();

In this example, we use multiple decorators to create an advanced electric fence system:

  1. @Zakres
    - ensures property values stay within the specified range
  2. @LogOperation
    - logs method calls
  3. @MierzCzasWykonania
    - measures method execution time
  4. @WalidujParametry
    - checks method parameter validity
  5. @Cachuj
    - caches method results in memory

Metadata and reflection with decorators

TypeScript also allows using metadata with decorators, enabling more advanced use cases. This requires enabling the

emitDecoratorMetadata
and
experimentalDecorators
options in the
tsconfig.json
file and installing the
reflect-metadata
package.

Here is an example of using metadata with decorators:

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

In this example, we use metadata and reflection to validate method parameters. The

@Parametr
decorator marks parameters that should be validated, and the
@WalidujDane
decorator checks the types of those parameters on every method call.

Practical example: Dinosaur examination management system

Now let's combine all the discussed techniques in one larger example - a dinosaur examination management system:

1// Property decorator ensuring unique IDs
2function UnikalneID(prefiks: string = "ID") {
3  return function(target: any, propertyKey: string) {
4    const generujID = () => `${prefiks}-${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}`] = generujID();
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 WalidujDataBadania(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
30  const oryginalnaMetoda = descriptor.value;
31
32  descriptor.value = function(...args: any[]) {
33    // Assume the first argument is the examination date
34    const dataBadania = args[0];
35
36    if (!(dataBadania instanceof Date)) {
37      throw new Error("First parameter must be a Date instance");
38    }
39
40    const teraz = new Date();
41
42    if (dataBadania > teraz) {
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 (dataBadania < thirtyDaysAgo) {
51      console.warn("Warning: Examination date is older than 30 days");
52    }
53
54    return oryginalnaMetoda.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 ostatnieSzczepienia = new Map<string, Date>();
65
66    const oryginalnaMetoda = descriptor.value;
67
68    descriptor.value = function(...args: any[]) {
69      // Assume first argument is dinosaur ID, second is vaccination date
70      const idDinosaura = args[0];
71      const dataSzczepienia = args[1];
72
73      if (ostatnieSzczepienia.has(idDinosaura)) {
74        const ostatniaSzczepionka = ostatnieSzczepienia.get(idDinosaura)!;
75        const differenceDays = Math.floor((dataSzczepienia.getTime() - ostatniaSzczepionka.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      ostatnieSzczepienia.set(idDinosaura, dataSzczepienia);
84
85      return oryginalnaMetoda.apply(this, args);
86    };
87
88    return descriptor;
89  };
90}
91
92// Examination results interface
93interface WynikiBadania {
94  temperatura: number;  // in degrees Celsius
95  weight: number;         // in kilograms
96  length: number;      // in meters
97  stanZdrowia: "Zdrowy" | "Lekkie objawy" | "Chory" | "Krytyczny";
98  uwagi?: string;
99}
100
101// Dinosaur examination class
102class BadanieDinosaura {
103  @UnikalneID("BAD")
104  idBadania: string;
105
106  constructor(
107    public idDinosaura: string,
108    public dataBadania: Date,
109    public wyniki: WynikiBadania,
110    public examiner: string
111  ) {}
112
113  @LogOperation
114  pobierzRaport(): string {
115    return `
116      === EXAMINATION REPORT ===
117      Examination ID: ${this.idBadania}
118      Dinosaur ID: ${this.idDinosaura}
119      Date: ${this.dataBadania.toLocaleString()}
120      Conducted by: ${this.examiner}
121
122      RESULTS:
123      - Temperature: ${this.wyniki.temperatura}°C
124      - Weight: ${this.wyniki.weight} kg
125      - Length: ${this.wyniki.length} m
126      - Health status: ${this.wyniki.stanZdrowia}
127      ${this.wyniki.uwagi ? `- Notes: ${this.wyniki.uwagi}` : ""}
128    `;
129  }
130}
131
132// Dinosaur vaccination class
133class SzczepienieDinosaura {
134  @UnikalneID("SZCZ")
135  idSzczepienia: string;
136
137  constructor(
138    public idDinosaura: string,
139    public dataSzczepienia: Date,
140    public genusSzczepionki: string,
141    public dawka: number,
142    public vaccinator: string
143  ) {}
144
145  @LogOperation
146  pobierzRaport(): string {
147    return `
148      === VACCINATION REPORT ===
149      Vaccination ID: ${this.idSzczepienia}
150      Dinosaur ID: ${this.idDinosaura}
151      Date: ${this.dataSzczepienia.toLocaleString()}
152      Vaccine type: ${this.genusSzczepionki}
153      Dose: ${this.dawka} ml
154      Conducted by: ${this.vaccinator}
155    `;
156  }
157}
158
159// Examination and vaccination registry
160class RejestrMedyczny {
161  private badania: BadanieDinosaura[] = [];
162  private szczepienia: SzczepienieDinosaura[] = [];
163
164  @LogOperation
165  @WalidujDataBadania
166  zarejestrujBadanie(
167    dataBadania: Date,
168    idDinosaura: string,
169    temperatura: number,
170    weight: number,
171    length: number,
172    stanZdrowia: "Zdrowy" | "Lekkie objawy" | "Chory" | "Krytyczny",
173    uwagi?: string,
174    examiner: string = "Dr. Harding"
175  ): BadanieDinosaura {
176    const wyniki: WynikiBadania = {
177      temperatura,
178      weight,
179      length,
180      stanZdrowia,
181      uwagi
182    };
183
184    const badanie = new BadanieDinosaura(idDinosaura, dataBadania, wyniki, examiner);
185    this.badania.push(badanie);
186
187    console.log(`Examination registered: ${badanie.idBadania} for dinosaur ${idDinosaura}`);
188
189    return badanie;
190  }
191
192  @LogOperation
193  @ControlFrequency(90) // Minimum 90 days between vaccinations
194  zarejestrujSzczepienie(
195    idDinosaura: string,
196    dataSzczepienia: Date,
197    genusSzczepionki: string,
198    dawka: number,
199    vaccinator: string = "Dr. Harding"
200  ): SzczepienieDinosaura {
201    const szczepienie = new SzczepienieDinosaura(
202      idDinosaura,
203      dataSzczepienia,
204      genusSzczepionki,
205      dawka,
206      vaccinator
207    );
208
209    this.szczepienia.push(szczepienie);
210
211    console.log(`Vaccination registered: ${szczepienie.idSzczepienia} for dinosaur ${idDinosaura}`);
212
213    return szczepienie;
214  }
215
216  @Cachuj
217  @LogOperation
218  getExaminationHistory(idDinosaura: string): BadanieDinosaura[] {
219    console.log(`Fetching examination history for dinosaur ${idDinosaura}...`);
220
221    // Simulating a long operation
222    for (let i = 0; i < 500000; i++) {
223      // CPU load
224    }
225
226    return this.badania.filter(badanie => badanie.idDinosaura === idDinosaura)
227      .sort((a, b) => b.dataBadania.getTime() - a.dataBadania.getTime());
228  }
229
230  @Cachuj
231  @LogOperation
232  getVaccinationHistory(idDinosaura: string): SzczepienieDinosaura[] {
233    console.log(`Fetching vaccination history for dinosaur ${idDinosaura}...`);
234
235    // Simulating a long operation
236    for (let i = 0; i < 500000; i++) {
237      // CPU load
238    }
239
240    return this.szczepienia.filter(szczepienie => szczepienie.idDinosaura === idDinosaura)
241      .sort((a, b) => b.dataSzczepienia.getTime() - a.dataSzczepienia.getTime());
242  }
243
244  @MierzCzasWykonania
245  @LogOperation
246  generujRaportMedyczny(idDinosaura: string): string {
247    const badania = this.getExaminationHistory(idDinosaura);
248    const szczepienia = this.getVaccinationHistory(idDinosaura);
249
250    let raport = `=== MEDICAL REPORT FOR DINOSAUR ${idDinosaura} ===
251
252`;
253
254    raport += `Number of examinations: ${badania.length}
255`;
256    raport += `Number of vaccinations: ${szczepienia.length}
257
258`;
259
260    if (badania.length > 0) {
261      const ostatnieBadanie = badania[0];
262      raport += `LAST EXAMINATION:
263`;
264      raport += `Date: ${ostatnieBadanie.dataBadania.toLocaleString()}
265`;
266      raport += `Health status: ${ostatnieBadanie.wyniki.stanZdrowia}
267`;
268      raport += `Temperature: ${ostatnieBadanie.wyniki.temperatura}°C
269`;
270      raport += `Weight: ${ostatnieBadanie.wyniki.weight} kg
271`;
272      raport += `Length: ${ostatnieBadanie.wyniki.length} m
273`;
274      if (ostatnieBadanie.wyniki.uwagi) {
275        raport += `Notes: ${ostatnieBadanie.wyniki.uwagi}
276`;
277      }
278      raport += "\n";
279    }
280
281    if (szczepienia.length > 0) {
282      const ostatnieSzczepienie = szczepienia[0];
283      raport += `LAST VACCINATION:
284`;
285      raport += `Date: ${ostatnieSzczepienie.dataSzczepienia.toLocaleString()}
286`;
287      raport += `Vaccine type: ${ostatnieSzczepienie.genusSzczepionki}
288`;
289      raport += `Dose: ${ostatnieSzczepienie.dawka} ml
290
291`;
292    }
293
294    raport += `EXAMINATION HISTORY:
295`;
296    badania.forEach((badanie, indeks) => {
297      raport += `${indeks + 1}. ${badanie.dataBadania.toLocaleDateString()} - ${badanie.wyniki.stanZdrowia}
298`;
299    });
300
301    raport += `
302VACCINATION HISTORY:
303`;
304    szczepienia.forEach((szczepienie, indeks) => {
305      raport += `${indeks + 1}. ${szczepienie.dataSzczepienia.toLocaleDateString()} - ${szczepienie.genusSzczepionki}
306`;
307    });
308
309    return raport;
310  }
311}
312
313// Testing the system
314const rejestrMedyczny = new RejestrMedyczny();
315
316// Registering examinations and vaccinations
317try {
318  // Past examination - OK
319  const badanie1 = rejestrMedyczny.zarejestrujBadanie(
320    new Date("2023-09-01"),
321    "TREX-001",
322    38.2,
323    7500,
324    12.5,
325    "Zdrowy",
326    "Routine examination",
327    "Dr. Harding"
328  );
329
330  // Future examination - Error
331  const badanie2 = rejestrMedyczny.zarejestrujBadanie(
332    new Date("2025-01-01"), // future
333    "TREX-001",
334    39.1,
335    7450,
336    12.5,
337    "Lekkie objawy",
338    "Slight fever"
339  );
340} catch (error) {
341  console.error(error.message);
342}
343
344// Registering vaccinations
345try {
346  // First vaccination
347  const szczepienie1 = rejestrMedyczny.zarejestrujSzczepienie(
348    "TREX-001",
349    new Date("2023-01-15"),
350    "Dinosaur flu vaccine",
351    50,
352    "Dr. Sattler"
353  );
354
355  // Second vaccination - too early, should throw error
356  const szczepienie2 = rejestrMedyczny.zarejestrujSzczepienie(
357    "TREX-001",
358    new Date("2023-03-15"), // Only 60 days later, minimum is 90 days
359    "Anti-parasite vaccine",
360    30
361  );
362} catch (error) {
363  console.error(error.message);
364}
365
366// Testing data retrieval
367console.log(rejestrMedyczny.getExaminationHistory("TREX-001")); // Full fetch
368console.log(rejestrMedyczny.getExaminationHistory("TREX-001")); // Uses cache
369
370// Generating medical report
371console.log(rejestrMedyczny.generujRaportMedyczny("TREX-001"));

In this elaborate example, we created a comprehensive system for managing dinosaur examinations and vaccinations in Jurassic Park, using various decorators:

  1. @UnikalneID
    - ensures each examination and vaccination has a unique ID
  2. @LogOperation
    - logs method calls
  3. @WalidujDataBadania
    - validates examination date correctness
  4. @ControlFrequency
    - ensures proper intervals between vaccinations
  5. @Cachuj
    - caches results of long-running operations
  6. @MierzCzasWykonania
    - measures report generation time

This system shows how method and property decorators can be used to create advanced business functionality while ensuring code consistency, data safety, and performance.

When to use method and property decorators

Method and property decorators are especially useful:

  1. When you want to add aspect-oriented functionality - logging, performance measurement, validation
  2. When you want to cache results - for computationally expensive methods
  3. When you need parameter validation - checking types, ranges, correctness
  4. When you want to control access - authorization, permission checking
  5. When you want to add metadata - for frameworks, ORMs, serialization
  6. When you need to monitor changes - tracking property changes

Limitations and issues

Like class decorators, method and property decorators have their limitations and issues:

  1. Harder debugging - code with many decorators can be harder to trace
  2. Typing problems - may require type casting (e.g., using
    as any
    )
  3. Hidden logic - implementation may be hidden in the decorator, making code harder to understand
  4. Application order - decorator order matters and can lead to subtle bugs
  5. Specification changes - the decorator specification is still evolving in JavaScript and TypeScript

Summary

Method and property decorators in TypeScript are powerful tools that allow adding extra functionality to class methods and properties in a declarative way. Just as operational protocols in Jurassic Park help control individual actions, method and property decorators help control and extend the behavior of individual class elements.

Thanks to decorators, we can create more modular, readable, and maintainable applications, isolating cross-cutting functionality such as logging, validation, or security. In large projects, well-designed decorators can significantly improve code quality and reduce redundancy.

However, like any advanced tool, method and property decorators should be used wisely. Excessive use of decorators can lead to code that is difficult to understand, debug, and test. The best practice is to use them where they genuinely provide benefits, not just for the sake of using them.

Go to CodeWorlds