JavaScript and TypeScript course Β· Module 7: Advanced TypeScript
Method and Property Decorators
In this lesson12
"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:
- Add additional behavior before or after method execution
- Validate method parameters
- Cache method results
- Measure method execution time
- Control property access
- Transform property values
- Add metadata to methods and properties
Method decorators
Method decorators are functions that take three parameters:
target- the class prototype (for instance methods) or the class constructor (for static methods)propertyKey- the method namedescriptor- 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 loggedIn 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:
target- the class prototype (for instance properties) or the class constructor (for static properties)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 -> OtwartaIn 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:
@Zakres- ensures property values stay within the specified range@LogOperation- logs method calls@MierzCzasWykonania- measures method execution time@WalidujParametry- checks method parameter validity@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} ===\n\n`;
251
252 raport += `Number of examinations: ${badania.length}\n`;
253 raport += `Number of vaccinations: ${szczepienia.length}\n\n`;
254
255 if (badania.length > 0) {
256 const ostatnieBadanie = badania[0];
257 raport += `LAST EXAMINATION:\n`;
258 raport += `Date: ${ostatnieBadanie.dataBadania.toLocaleString()}\n`;
259 raport += `Health status: ${ostatnieBadanie.wyniki.stanZdrowia}\n`;
260 raport += `Temperature: ${ostatnieBadanie.wyniki.temperatura}Β°C\n`;
261 raport += `Weight: ${ostatnieBadanie.wyniki.weight} kg\n`;
262 raport += `Length: ${ostatnieBadanie.wyniki.length} m\n`;
263 if (ostatnieBadanie.wyniki.uwagi) {
264 raport += `Notes: ${ostatnieBadanie.wyniki.uwagi}\n`;
265 }
266 raport += "\n";
267 }
268
269 if (szczepienia.length > 0) {
270 const ostatnieSzczepienie = szczepienia[0];
271 raport += `LAST VACCINATION:\n`;
272 raport += `Date: ${ostatnieSzczepienie.dataSzczepienia.toLocaleString()}\n`;
273 raport += `Vaccine type: ${ostatnieSzczepienie.genusSzczepionki}\n`;
274 raport += `Dose: ${ostatnieSzczepienie.dawka} ml\n\n`;
275 }
276
277 raport += `EXAMINATION HISTORY:\n`;
278 badania.forEach((badanie, indeks) => {
279 raport += `${indeks + 1}. ${badanie.dataBadania.toLocaleDateString()} - ${badanie.wyniki.stanZdrowia}\n`;
280 });
281
282 raport += `\nVACCINATION HISTORY:\n`;
283 szczepienia.forEach((szczepienie, indeks) => {
284 raport += `${indeks + 1}. ${szczepienie.dataSzczepienia.toLocaleDateString()} - ${szczepienie.genusSzczepionki}\n`;
285 });
286
287 return raport;
288 }
289}
290
291// Testing the system
292const rejestrMedyczny = new RejestrMedyczny();
293
294// Registering examinations and vaccinations
295try {
296 // Past examination - OK
297 const badanie1 = rejestrMedyczny.zarejestrujBadanie(
298 new Date("2023-09-01"),
299 "TREX-001",
300 38.2,
301 7500,
302 12.5,
303 "Zdrowy",
304 "Routine examination",
305 "Dr. Harding"
306 );
307
308 // Future examination - Error
309 const badanie2 = rejestrMedyczny.zarejestrujBadanie(
310 new Date("2025-01-01"), // future
311 "TREX-001",
312 39.1,
313 7450,
314 12.5,
315 "Lekkie objawy",
316 "Slight fever"
317 );
318} catch (error) {
319 console.error(error.message);
320}
321
322// Registering vaccinations
323try {
324 // First vaccination
325 const szczepienie1 = rejestrMedyczny.zarejestrujSzczepienie(
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 szczepienie2 = rejestrMedyczny.zarejestrujSzczepienie(
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(rejestrMedyczny.getExaminationHistory("TREX-001")); // Full fetch
346console.log(rejestrMedyczny.getExaminationHistory("TREX-001")); // Uses cache
347
348// Generating medical report
349console.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:
@UnikalneID- ensures each examination and vaccination has a unique ID@LogOperation- logs method calls@WalidujDataBadania- validates examination date correctness@ControlFrequency- ensures proper intervals between vaccinations@Cachuj- caches results of long-running operations@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:
- When you want to add aspect-oriented functionality - logging, performance measurement, validation
- When you want to cache results - for computationally expensive methods
- When you need parameter validation - checking types, ranges, correctness
- When you want to control access - authorization, permission checking
- When you want to add metadata - for frameworks, ORMs, serialization
- When you need to monitor changes - tracking property changes
Limitations and issues
Like class decorators, method and property decorators have their limitations and issues:
- Harder debugging - code with many decorators can be harder to trace
- Typing problems - may require type casting (e.g., using
as any) - Hidden logic - implementation may be hidden in the decorator, making code harder to understand
- Application order - decorator order matters and can lead to subtle bugs
- 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.
Code for this lesson: index.ts
1// Method and property decorators - Supervising Jurassic Park operations
2// Note: Decorators require "experimentalDecorators": true in tsconfig.json
3
4// === METHOD DECORATOR - Logging operations ===
5function LogOperation(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
6 const originalMethod = descriptor.value;
7
8 descriptor.value = function (...args: any[]) {
9 console.log("[LOG] Call: " + 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] Finished: " + propertyKey + " (" + duration + "ms)");
14 return result;
15 };
16
17 return descriptor;
18}
19
20// === METHOD DECORATOR - Validating parameters ===
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("Invalid sector: " + sectorId);
28 }
29 return originalMethod.apply(this, args);
30 };
31
32 return descriptor;
33}
34
35// === METHOD DECORATOR - Memoization (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] Cached result for " + 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// === A CLASS WITH DECORATORS ===
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: "empty" };
69 return { sector: sectorId, ...dino, status: "active" };
70 }
71
72 @LogOperation
73 @Memoize
74 calculateFeedingCost(species: string, days: number): number {
75 // Simulating expensive calculations
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// === TESTING ===
86const tracker = new DinosaurTracker();
87
88console.log("=== Checking sectors ===");
89console.log(tracker.checkSector("A1"));
90console.log(tracker.checkSector("B3"));
91
92console.log("\n=== Feeding costs ===");
93console.log("T-Rex 7 days:", tracker.calculateFeedingCost("T-Rex", 7));
94console.log("T-Rex 7 days (cache):", tracker.calculateFeedingCost("T-Rex", 7));
95
96console.log("\n=== Sector validation ===");
97try {
98 tracker.checkSector("invalid");
99} catch (e: any) {
100 console.error("Error:", e.message);
101}
102
103// TODO: Create a decorator @ReadOnly that prevents a method from being overwritten
104// TODO: Create a decorator @Throttle(ms) that limits how often a method can be calledSpotted a mistake in this lesson?
Check yourself
Answer the questions from this lesson. Pick an answer to see right away whether it is correct.
1. Which decorator in TypeScript receives the class constructor as an argument and can extend or replace it?
2. What three arguments does a method decorator receive in TypeScript?
Hands-on tasks in the game
- Code editor
Implement the Trackable (class), Authorize (method), and MaxLength (property) decorators according to the instructions in the code.
- Vertical ordering
Order the steps for creating a method decorator in TypeScript: