"Standardization and reuse are the key to success in the park," emphasizes Simon Masrani, the new owner of Jurassic Park. "We need systems that can be easily adapted to different dinosaur species, different parts of the park, and different security levels, but all according to uniform standards."
In the world of TypeScript, just as in managing Jurassic Park, we often need to create similar but slightly different decorators for different use cases. Decorator factories allow us to create highly configurable decorators that we can tailor to specific needs while maintaining a consistent structure and logic.
A decorator factory is simply a function that returns a decorator, accepting configuration parameters. This allows creating decorators "on demand" with different behavior variants, based on the same template.
Main advantages of decorator factories:
A decorator factory typically has the following structure:
1// Class decorator factory
2function fabrykaDekoratoraKlasy(options: OpcjeFabryki) {
3 // Returns a class decorator
4 return function<T extends new (...args: any[]) => any>(target: T) {
5 // Decorator implementation using options
6 //
7 return target; // or modified class
8 };
9}
10
11// Method decorator factory
12function fabrykaDekoratoraMetody(options: OpcjeFabryki) {
13 // Returns a method decorator
14 return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
15 // Decorator implementation using options
16 //
17 return descriptor;
18 };
19}
20
21// Property decorator factory
22function propertyDecoratorFactory(options: OpcjeFabryki) {
23 // Returns a property decorator
24 return function(target: any, propertyKey: string) {
25 // Decorator implementation using options
26 //
27 };
28}Let's see how we can use decorator factories in the context of managing Jurassic Park.
1// Park subsystem types
2type PodsystemParku = "Security" | "Zagrody" | "Laboratorium" | "Transport" | "Zasilanie";
3
4// Logging levels
5type PoziomLogowania = "Debug" | "Info" | "Warning" | "Error" | "Krytyczny";
6
7// Options interface for the logging decorator factory
8interface OpcjeLogowania {
9 podsystem: PodsystemParku;
10 poziom: PoziomLogowania;
11 registerCallOrder?: boolean;
12 czasWykonania?: boolean;
13}
14
15// Logging decorator factory
16function LogowanieSystemu(options: OpcjeLogowania) {
17 const { podsystem, poziom, registerCallOrder = false, czasWykonania = false } = options;
18
19 // Call counter for each method
20 const callCounter: Record<string, number> = {};
21
22 // We return a method decorator
23 return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
24 // Save the original method
25 const oryginalnaMetoda = descriptor.value;
26
27 // Define a new method
28 descriptor.value = function(...args: any[]) {
29 // Record call order if needed
30 if (registerCallOrder) {
31 if (!callCounter[propertyKey]) {
32 callCounter[propertyKey] = 0;
33 }
34 callCounter[propertyKey]++;
35 console.log(`[${podsystem}] [Call #${callCounter[propertyKey]}] ${propertyKey}`);
36 }
37
38 // Initial logging
39 console.log(`[${poziom}] [${podsystem}] Calling ${propertyKey}(${JSON.stringify(args)})`);
40
41 // Time measurement if needed
42 const startTime = czasWykonania ? performance.now() : 0;
43
44 try {
45 // Call the original method
46 const wynik = oryginalnaMetoda.apply(this, args);
47
48 // Log after successful execution
49 console.log(`[${poziom}] [${podsystem}] Completed ${propertyKey} - Status: Success`);
50
51 // Log execution time if needed
52 if (czasWykonania) {
53 const endTime = performance.now();
54 console.log(`[${podsystem}] Execution time ${propertyKey}: ${(endTime - startTime).toFixed(2)}ms`);
55 }
56
57 return wynik;
58 } catch (error) {
59 // Log on error
60 console.error(`[${poziom}] [${podsystem}] Error in ${propertyKey}: ${error.message}`);
61
62 // Propagate the error
63 throw error;
64 }
65 };
66
67 return descriptor;
68 };
69}
70
71// Class managing the enclosure system
72class EnclosuresSystem {
73 // We use the factory to create different logging decorators
74 @LogowanieSystemu({ podsystem: "Zagrody", poziom: "Info" })
75 openEnclosure(idZagrody: string): boolean {
76 console.log(`Opening enclosure ${idZagrody}`);
77 return true;
78 }
79
80 @LogowanieSystemu({ podsystem: "Zagrody", poziom: "Warning", czasWykonania: true })
81 closeEnclosure(idZagrody: string): boolean {
82 console.log(`Closing enclosure ${idZagrody}`);
83
84 // Simulate longer execution
85 for (let i = 0; i < 1000000; i++) {
86 // CPU load
87 }
88
89 return true;
90 }
91
92 @LogowanieSystemu({ podsystem: "Zagrody", poziom: "Krytyczny", registerCallOrder: true, czasWykonania: true })
93 securityMode(idZagrody: string, reason: string): boolean {
94 console.log(`Activating safety mode for enclosure ${idZagrody}. Reason: ${reason}`);
95
96 // Simulate complex operation
97 for (let i = 0; i < 2000000; i++) {
98 // CPU load
99 }
100
101 return true;
102 }
103}
104
105// Class managing the security system
106class SecuritySystem {
107 @LogowanieSystemu({ podsystem: "Security", poziom: "Info" })
108 checkStatus(): { status: string, aktywneAlarmy: number } {
109 return { status: "Normalny", aktywneAlarmy: 0 };
110 }
111
112 @LogowanieSystemu({ podsystem: "Security", poziom: "Warning", czasWykonania: true })
113 aktywujAlarm(area: string, poziom: number): boolean {
114 console.log(`Activating alarm in area ${area} at level ${poziom}`);
115 return true;
116 }
117
118 @LogowanieSystemu({ podsystem: "Security", poziom: "Krytyczny", registerCallOrder: true })
119 ewakuacja(area: string): boolean {
120 console.log(`Evacuation protocol for area ${area}`);
121 return true;
122 }
123}
124
125// Using classes with decorators
126const enclosuresSystem = new EnclosuresSystem();
127const securitySystem = new SecuritySystem();
128
129// Testing
130enclosuresSystem.openEnclosure("A-001");
131enclosuresSystem.closeEnclosure("A-001");
132enclosuresSystem.securityMode("A-001", "Test protocol");
133enclosuresSystem.securityMode("A-001", "Ponowny test"); // Will show this is the second call
134
135securitySystem.checkStatus();
136securitySystem.aktywujAlarm("Sektor B", 2);
137securitySystem.ewakuacja("Sektor B");In this example, the
LogowanieSystemu decorator factory creates method logging decorators that can be adapted to different subsystems, logging levels, and additional options such as recording call order or measuring execution time.1// Options interface for the validation decorator factory
2interface OpcjeWalidacji<T> {
3 walidator: (value: T) => boolean;
4 errorMessage: string;
5 exception?: typeof Error;
6 transformacja?: (value: T) => T;
7}
8
9// Method parameter validation decorator factory
10function WalidujParametr<T>(options: OpcjeWalidacji<T>) {
11 const { walidator, errorMessage, exception = Error, transformacja } = options;
12
13 // We return a method parameter decorator
14 return function(target: Object, propertyKey: string, parameterIndex: number) {
15 // Metadata key
16 const kluczWalidacji = `walidacja_${propertyKey}_param_${parameterIndex}`;
17
18 // Save validation metadata
19 const walidacje = Reflect.getOwnMetadata(kluczWalidacji, target) || [];
20
21 // Add new validation
22 walidacje.push({
23 indeks: parameterIndex,
24 walidator,
25 errorMessage,
26 exception,
27 transformacja
28 });
29
30 // Save updated metadata
31 Reflect.defineMetadata(kluczWalidacji, walidacje, target);
32
33 // Check if a method decorator already exists
34 if (!Reflect.hasMetadata(`walidacja_${propertyKey}_decorated`, target)) {
35 // Mark the method as already decorated
36 Reflect.defineMetadata(`walidacja_${propertyKey}_decorated`, true, target);
37
38 // Get the method descriptor
39 const oryginalnaMetoda = target[propertyKey];
40
41 // Override the method
42 target[propertyKey] = function(...args: any[]) {
43 // Get all validations for this method
44 const walidacje = Reflect.getOwnMetadata(kluczWalidacji, target) || [];
45
46 // Apply validations
47 for (const walidacja of walidacje) {
48 const { indeks, walidator, errorMessage, exception, transformacja } = walidacja;
49
50 // If the value is provided, validate it
51 if (indeks < args.length) {
52 const value = args[indeks];
53
54 // Check validation
55 if (!walidator(value)) {
56 throw new exception(`Validation error for parameter ${indeks} in method ${propertyKey}: ${errorMessage}`);
57 }
58
59 // Apply transformation if provided
60 if (transformacja) {
61 args[indeks] = transformacja(value);
62 }
63 }
64 }
65
66 // Call the original method
67 return oryginalnaMetoda.apply(this, args);
68 };
69 }
70 };
71}
72
73// Set of predefined validators
74// Non-empty string validator
75function NiePusty() {
76 return WalidujParametr<string>({
77 walidator: (str) => str !== null && str !== undefined && str.trim() !== '',
78 errorMessage: "Value nie may be pusta"
79 });
80}
81
82// Minimum length validator
83function MinLength(length: number) {
84 return WalidujParametr<string>({
85 walidator: (str) => str !== null && str !== undefined && str.length >= length,
86 errorMessage: `Value musi have co najmniej ${length} characters`
87 });
88}
89
90// Numeric range validator
91function Zakres(min: number, max: number) {
92 return WalidujParametr<number>({
93 walidator: (num) => num >= min && num <= max,
94 errorMessage: `Value musi be w zakresie od ${min} do ${max}`,
95 // Optionally we can clamp the value to the range
96 transformacja: (num) => Math.min(Math.max(num, min), max)
97 });
98}
99
100// Positive number validator
101function Pozytywna() {
102 return WalidujParametr<number>({
103 walidator: (num) => num > 0,
104 errorMessage: "Value musi be dodatnia"
105 });
106}
107
108// Date format validator
109function FormatDaty(format: RegExp = /^d{4}-d{2}-d{2}$/) {
110 return WalidujParametr<string>({
111 walidator: (str) => format.test(str),
112 errorMessage: "Value musi be w formacie daty YYYY-MM-DD"
113 });
114}
115
116// Class for managing dinosaur data
117class DinosaurManagement {
118 // Create a new dinosaur
119 dodajDinosaura(
120 @NiePusty() @MinLength(3) nazwa: string,
121 @NiePusty() gatunek: string,
122 @Pozytywna() @Zakres(0, 100) wiek: number,
123 @Pozytywna() weight: number,
124 @FormatDaty() dataDodania: string
125 ): string {
126 const idDinosaura = `DINO-${Date.now()}`;
127
128 console.log(`Added new dinosaur:`);
129 console.log(`- ID: ${idDinosaura}`);
130 console.log(`- Name: ${nazwa}`);
131 console.log(`- Species: ${gatunek}`);
132 console.log(`- Age: ${wiek} years`);
133 console.log(`- Weight: ${weight} kg`);
134 console.log(`- Date added: ${dataDodania}`);
135
136 return idDinosaura;
137 }
138
139 // Update dinosaur data
140 aktualizujDinosaura(
141 @NiePusty() idDinosaura: string,
142 @Zakres(0, 100) wiek?: number,
143 @Pozytywna() weight?: number
144 ): boolean {
145 console.log(`Updating dinosaur ${idDinosaura}:`);
146
147 if (wiek !== undefined) {
148 console.log(`- New age: ${wiek} years`);
149 }
150
151 if (weight !== undefined) {
152 console.log(`- New weight: ${weight} kg`);
153 }
154
155 return true;
156 }
157
158 // Transfer dinosaur to a new enclosure
159 moveDinosaur(
160 @NiePusty() idDinosaura: string,
161 @NiePusty() doZagrody: string,
162 @FormatDaty() dataPrzeniesienia: string
163 ): boolean {
164 console.log(`Transferring dinosaur ${idDinosaura} to enclosure ${doZagrody} on ${dataPrzeniesienia}`);
165 return true;
166 }
167}
168
169// Testing the class with parameter validation
170const dinoManagement = new DinosaurManagement();
171
172try {
173 // Correct call
174 dinoManagement.dodajDinosaura(
175 "Blue",
176 "Velociraptor",
177 5,
178 150,
179 "2023-09-15"
180 );
181
182 // Incorrect call - empty name
183 dinoManagement.dodajDinosaura(
184 "", // Empty name
185 "Tyrannosaurus",
186 8,
187 7500,
188 "2023-09-15"
189 );
190} catch (error) {
191 console.error(error.message);
192}
193
194try {
195 // Correct call
196 dinoManagement.aktualizujDinosaura("DINO-123", 6, 160);
197
198 // Incorrect call - negative weight
199 dinoManagement.aktualizujDinosaura("DINO-123", 6, -100);
200} catch (error) {
201 console.error(error.message);
202}
203
204try {
205 // Correct call
206 dinoManagement.moveDinosaur("DINO-123", "B-002", "2023-09-20");
207
208 // Incorrect call - invalid date format
209 dinoManagement.moveDinosaur("DINO-123", "B-003", "20/09/2023");
210} catch (error) {
211 console.error(error.message);
212}In this example, we created a parameter validation decorator factory that allows creating different validators such as
NiePusty, MinLength, Zakres, Pozytywna, and FormatDaty. Thanks to this, we can easily apply validation to method parameters in a declarative way.1// Park permission levels
2enum PermissionLevel {
3 Guest = 0,
4 Pracownik = 1,
5 Kierownik = 2,
6 Administrator = 3
7}
8
9// System actions
10type AkcjaSystemu = "odczyt" | "zapis" | "usuwanie" | "administracja";
11
12// Mapping permission levels to allowed actions
13const permissionsMap: Record<PermissionLevel, AkcjaSystemu[]> = {
14 [PermissionLevel.Guest]: ["odczyt"],
15 [PermissionLevel.Pracownik]: ["odczyt", "zapis"],
16 [PermissionLevel.Kierownik]: ["odczyt", "zapis", "usuwanie"],
17 [PermissionLevel.Administrator]: ["odczyt", "zapis", "usuwanie", "administracja"]
18};
19
20// Security context interface
21interface SecurityContext {
22 user: string;
23 permissionLevel: PermissionLevel;
24 tokeny?: string[];
25}
26
27// Permission denied error
28class PermissionError extends Error {
29 constructor(akcja: string, requiredLevel: PermissionLevel, aktualnyPoziom: PermissionLevel) {
30 super(`No permission for action ${akcja}. Required level: ${PermissionLevel[requiredLevel]}, current: ${PermissionLevel[aktualnyPoziom]}`);
31 this.name = "PermissionError";
32 }
33}
34
35// Permission decorator factory
36function WymagaUprawnienia(requiredAkcja: AkcjaSystemu, errorMessage?: string) {
37 return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
38 const oryginalnaMetoda = descriptor.value;
39
40 descriptor.value = function(this: { securityContext?: SecurityContext }, ...args: any[]) {
41 // Check if we have a security context
42 if (!this.securityContext) {
43 throw new Error("Brak kontekstu security");
44 }
45
46 const { permissionLevel } = this.securityContext;
47
48 // Check if the user has the action for their permission level
49 const dozwoloneAkcje = permissionsMap[permissionLevel] || [];
50
51 if (!dozwoloneAkcje.includes(requiredAkcja)) {
52 // Find the minimum permission level needed for this action
53 let minimalnyPoziom: PermissionLevel = PermissionLevel.Administrator;
54
55 for (const [poziom, akcje] of Object.entries(permissionsMap)) {
56 if (akcje.includes(requiredAkcja) && Number(poziom) < minimalnyPoziom) {
57 minimalnyPoziom = Number(poziom) as PermissionLevel;
58 }
59 }
60
61 throw new PermissionError(
62 requiredAkcja,
63 minimalnyPoziom,
64 permissionLevel
65 );
66 }
67
68 // Call the original method
69 return oryginalnaMetoda.apply(this, args);
70 };
71
72 return descriptor;
73 };
74}
75
76// Simplified versions for frequently used levels
77function ForGuests() {
78 return WymagaUprawnienia("odczyt");
79}
80
81function ForEmployees() {
82 return WymagaUprawnienia("zapis");
83}
84
85function ForManagers() {
86 return WymagaUprawnienia("usuwanie");
87}
88
89function ForAdministrators() {
90 return WymagaUprawnienia("administracja");
91}
92
93// Park management system class
94class ParkManagementSystemClass {
95 securityContext?: SecurityContext;
96
97 constructor() {
98 // No security context by default
99 }
100
101 // Method for setting the security context
102 zaloguj(user: string, permissionLevel: PermissionLevel): void {
103 this.securityContext = { user, permissionLevel };
104 console.log(`User ${user} logged in with permission level: ${PermissionLevel[permissionLevel]}`);
105 }
106
107 // Method accessible to everyone
108 @ForGuests()
109 pobierzInformacjePubliczne(): string {
110 return "Informacje publiczne o parku";
111 }
112
113 // Method accessible to employees and above
114 @ForEmployees()
115 aktualizujDaneDinosaura(idDinosaura: string, dane: any): boolean {
116 console.log(`Updating dinosaur data ${idDinosaura}`);
117 return true;
118 }
119
120 // Method accessible to managers and above
121 @ForManagers()
122 removeDinosaur(idDinosaura: string): boolean {
123 console.log(`Deleting dinosaur ${idDinosaura}`);
124 return true;
125 }
126
127 // Method accessible only to administrators
128 @ForAdministrators()
129 zmianaKonfiguracji(klucz: string, value: any): boolean {
130 console.log(`Configuration change: ${klucz} = ${value}`);
131 return true;
132 }
133
134 // Method using the factory directly
135 @WymagaUprawnienia("administracja", "Tylko administratorzy can restart system")
136 restartSystemu(): boolean {
137 console.log("Restarting system...");
138 return true;
139 }
140}
141
142// Testing the permission system
143const system = new ParkManagementSystemClass();
144
145// Test for guest
146console.log("--- Test for guest ---");
147system.zaloguj("JohnVisitor", PermissionLevel.Guest);
148
149try {
150 console.log(system.pobierzInformacjePubliczne()); // Should work
151 system.aktualizujDaneDinosaura("DINO-123", { wiek: 5 }); // Should throw error
152} catch (error) {
153 console.error(error.message);
154}
155
156// Test for employee
157console.log("\n--- Test for employee ---");
158system.zaloguj("JaneEmployee", PermissionLevel.Pracownik);
159
160try {
161 console.log(system.pobierzInformacjePubliczne()); // Should work
162 system.aktualizujDaneDinosaura("DINO-123", { wiek: 5 }); // Should work
163 system.removeDinosaur("DINO-123"); // Should throw error
164} catch (error) {
165 console.error(error.message);
166}
167
168// Test for manager
169console.log("\n--- Test for manager ---");
170system.zaloguj("BobManager", PermissionLevel.Kierownik);
171
172try {
173 system.removeDinosaur("DINO-123"); // Should work
174 system.zmianaKonfiguracji("maxDino", 100); // Should throw error
175} catch (error) {
176 console.error(error.message);
177}
178
179// Test for administrator
180console.log("\n--- Test for administrator ---");
181system.zaloguj("AliceAdmin", PermissionLevel.Administrator);
182
183try {
184 system.zmianaKonfiguracji("maxDino", 100); // Should work
185 system.restartSystemu(); // Should work
186} catch (error) {
187 console.error(error.message);
188}In this example, we created a permission decorator factory that enables controlling access to methods based on user permission levels. Based on it, we created simplified decorators for frequently used permission levels, such as
ForGuests, ForEmployees, ForManagers, and ForAdministrators.1// Park resource types
2type TypZasobu = "dinosaur" | "zagroda" | "budynek" | "pojazd" | "equipment";
3
4// Resource usage tracking interface
5interface TrackedUsage {
6 zasobId: string;
7 typ: TypZasobu;
8 startTime: Date;
9 endTime?: Date;
10 user: string;
11 status: "inUse" | "completed" | "anulowane";
12}
13
14// Global resource usage registry
15class ResourceUsageRegistry {
16 private static instancja: ResourceUsageRegistry;
17 private historia: TrackedUsage[] = [];
18 private aktywneWykorzystanie: Map<string, TrackedUsage> = new Map();
19
20 private constructor() {}
21
22 // Method to get the singleton
23 static getInstance(): ResourceUsageRegistry {
24 if (!ResourceUsageRegistry.instancja) {
25 ResourceUsageRegistry.instancja = new ResourceUsageRegistry();
26 }
27 return ResourceUsageRegistry.instancja;
28 }
29
30 // Method to start tracking
31 startTracking(zasobId: string, typ: TypZasobu, user: string): string {
32 const usageId = `USE-${Date.now()}-${Math.floor(Math.random() * 1000)}`;
33
34 const tracking: TrackedUsage = {
35 zasobId,
36 typ,
37 startTime: new Date(),
38 user,
39 status: "inUse"
40 };
41
42 this.aktywneWykorzystanie.set(usageId, tracking);
43
44 console.log(`Started tracking resource ${typ} ${zasobId} by ${user}`);
45
46 return usageId;
47 }
48
49 // Method to stop tracking
50 endTracking(usageId: string, sukces: boolean = true): boolean {
51 if (!this.aktywneWykorzystanie.has(usageId)) {
52 console.warn(`No active tracking found for ID: ${usageId}`);
53 return false;
54 }
55
56 const tracking = this.aktywneWykorzystanie.get(usageId)!;
57 tracking.endTime = new Date();
58 tracking.status = sukces ? "completed" : "anulowane";
59
60 this.historia.push(tracking);
61 this.aktywneWykorzystanie.delete(usageId);
62
63 console.log(`Stopped tracking resource ${tracking.typ} ${tracking.zasobId} by ${tracking.user} with status: ${tracking.status}`);
64
65 return true;
66 }
67
68 // Method to check if a resource is currently in use
69 isResourceInUse(zasobId: string, typ: TypZasobu): boolean {
70 for (const [usageId, tracking] of this.aktywneWykorzystanie.entries()) {
71 if (tracking.zasobId === zasobId && tracking.typ === typ) {
72 return true;
73 }
74 }
75 return false;
76 }
77
78 // Method to get resource usage history
79 getResourceHistory(zasobId: string, typ: TypZasobu): TrackedUsage[] {
80 return this.historia.filter(tracking =>
81 tracking.zasobId === zasobId && tracking.typ === typ
82 );
83 }
84
85 // Method to get the number of active usages
86 countActiveUses(): number {
87 return this.aktywneWykorzystanie.size;
88 }
89}
90
91// Options interface for the resource tracking decorator factory
92interface ResourceTrackingOptions {
93 typZasobu: TypZasobu;
94 checkAccess?: boolean;
95 indeksIdZasobu?: number;
96 unavailabilityMessage?: string;
97}
98
99// Resource tracking decorator factory
100function TrackedResourceUsage(options: ResourceTrackingOptions) {
101 const {
102 typZasobu,
103 checkAccess = false,
104 indeksIdZasobu = 0,
105 unavailabilityMessage = "Resource jest obecnie unavailable"
106 } = options;
107
108 return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
109 const oryginalnaMetoda = descriptor.value;
110
111 descriptor.value = function(...args: any[]) {
112 // Get the resource ID from parameters
113 const zasobId = args[indeksIdZasobu];
114
115 if (!zasobId) {
116 throw new Error(`No resource ID provided (parameter #${indeksIdZasobu})`);
117 }
118
119 // Get the registry
120 const rejestr = ResourceUsageRegistry.getInstance();
121
122 // If availability check is needed
123 if (checkAccess && rejestr.isResourceInUse(zasobId, typZasobu)) {
124 throw new Error(`${unavailabilityMessage} (${typZasobu} ${zasobId})`);
125 }
126
127 // Get the username from the object if available
128 const user = this.user || "system";
129
130 // Start tracking
131 const usageId = rejestr.startTracking(zasobId, typZasobu, user);
132
133 try {
134 // Call the original method
135 const wynik = oryginalnaMetoda.apply(this, args);
136
137 // End tracking with success
138 rejestr.endTracking(usageId, true);
139
140 return wynik;
141 } catch (error) {
142 // End tracking with error
143 rejestr.endTracking(usageId, false);
144
145 // Propagate the error
146 throw error;
147 }
148 };
149
150 return descriptor;
151 };
152}
153
154// Helper functions for frequently used resource types
155function TrackedDinosaurUsage(options: Omit<ResourceTrackingOptions, "typZasobu"> = {}) {
156 return TrackedResourceUsage({ ...options, typZasobu: "dinosaur" });
157}
158
159function TrackedEnclosureUsage(options: Omit<ResourceTrackingOptions, "typZasobu"> = {}) {
160 return TrackedResourceUsage({ ...options, typZasobu: "zagroda" });
161}
162
163function TrackedVehicleUsage(options: Omit<ResourceTrackingOptions, "typZasobu"> = {}) {
164 return TrackedResourceUsage({
165 ...options,
166 typZasobu: "pojazd",
167 checkAccess: true,
168 unavailabilityMessage: "Pojazd jest obecnie w use"
169 });
170}
171
172function TrackedEquipmentUsage(options: Omit<ResourceTrackingOptions, "typZasobu"> = {}) {
173 return TrackedResourceUsage({
174 ...options,
175 typZasobu: "equipment",
176 checkAccess: true,
177 unavailabilityMessage: "Equipment jest obecnie w use"
178 });
179}
180
181// Park resource management class
182class ParkResourcesManagement {
183 user: string;
184
185 constructor(user: string) {
186 this.user = user;
187 }
188
189 // Tracking dinosaur examination
190 @TrackedDinosaurUsage()
191 conductResearchDinosaura(idDinosaura: string): { status: string, temperatura: number } {
192 console.log(`Performing examination of dinosaur ${idDinosaura}`);
193
194 // Simulate long process
195 for (let i = 0; i < 1000000; i++) {
196 // CPU load
197 }
198
199 return { status: "zdrowy", temperatura: 38.5 };
200 }
201
202 // Tracking dinosaur feeding
203 @TrackedDinosaurUsage()
204 feedDinosaura(idDinosaura: string, typPokarmu: string, amount: number): boolean {
205 console.log(`Feeding dinosaur ${idDinosaura} with ${typPokarmu} (${amount} kg)`);
206
207 // Simulate process
208 for (let i = 0; i < 500000; i++) {
209 // CPU load
210 }
211
212 return true;
213 }
214
215 // Tracking enclosure inspection
216 @TrackedEnclosureUsage()
217 inspectEnclosure(idZagrody: string): { status: string, problemy: string[] } {
218 console.log(`Inspecting enclosure ${idZagrody}`);
219
220 // Simulate inspection
221 for (let i = 0; i < 800000; i++) {
222 // CPU load
223 }
224
225 return { status: "OK", problemy: [] };
226 }
227
228 // Tracking vehicle usage
229 @TrackedVehicleUsage()
230 useVehicle(idPojazdu: string, cel: string): boolean {
231 console.log(`Using vehicle ${idPojazdu} for destination: ${cel}`);
232
233 // Simulate driving
234 for (let i = 0; i < 1500000; i++) {
235 // CPU load
236 }
237
238 return true;
239 }
240
241 // Tracking equipment usage
242 @TrackedEquipmentUsage({ indeksIdZasobu: 1 })
243 useEquipment(cel: string, equipmentId: string): boolean {
244 console.log(`Using equipment ${equipmentId} for purpose: ${cel}`);
245
246 // Simulate usage
247 for (let i = 0; i < 700000; i++) {
248 // CPU load
249 }
250
251 return true;
252 }
253
254 // Direct factory usage with custom options
255 @TrackedResourceUsage({
256 typZasobu: "budynek",
257 checkAccess: true,
258 indeksIdZasobu: 0,
259 unavailabilityMessage: "Budynek jest obecnie w konserwacji"
260 })
261 performBuildingMaintenance(idBudynku: string, typKonserwacji: string): boolean {
262 console.log(`Maintenance of building ${idBudynku}: ${typKonserwacji}`);
263
264 // Simulate maintenance
265 for (let i = 0; i < 2000000; i++) {
266 // CPU load
267 }
268
269 return true;
270 }
271}
272
273// Testing the resource tracking system
274async function testResourceTracking() {
275 const pracownik1 = new ParkResourcesManagement("John");
276 const pracownik2 = new ParkResourcesManagement("Alice");
277
278 // Test dinosaur examination
279 console.log("--- Test: Dinosaur examination ---");
280 await pracownik1.conductResearchDinosaura("DINO-001");
281
282 // Test dinosaur feeding
283 console.log("\n--- Test: Dinosaur feeding ---");
284 await pracownik2.feedDinosaura("DINO-001", "meat", 50);
285
286 // Test enclosure inspection
287 console.log("\n--- Test: Enclosure inspection ---");
288 await pracownik1.inspectEnclosure("ZAG-001");
289
290 // Test vehicle usage
291 console.log("\n--- Test: Vehicle usage ---");
292
293 try {
294 // First employee uses the vehicle
295 console.log("Employee 1 uses the vehicle...");
296 const obietnica1 = pracownik1.useVehicle("JEEP-001", "Round parku");
297
298 // We try to use the same vehicle by another employee
299 console.log("Attempt to use the same vehicle by Employee 2...");
300 const obietnica2 = pracownik2.useVehicle("JEEP-001", "Transport");
301
302 await Promise.all([obietnica1, obietnica2]);
303 } catch (error) {
304 console.error(`Error: ${error.message}`);
305 }
306
307 // Test equipment usage
308 console.log("\n--- Test: Equipment usage ---");
309 await pracownik1.useEquipment("Badanie", "EQUIP-001");
310
311 // Test building maintenance
312 console.log("\n--- Test: Building maintenance ---");
313 await pracownik2.performBuildingMaintenance("BLD-001", "Elektryka");
314
315 // Check number of active usages
316 const rejestr = ResourceUsageRegistry.getInstance();
317 console.log(`\nActive usages: ${rejestr.countActiveUses()}`);
318
319 // Check resource history
320 console.log("\nHistory for dinosaur DINO-001:");
321 const historiaDino = rejestr.getResourceHistory("DINO-001", "dinosaur");
322 console.log(`Number of entries: ${historiaDino.length}`);
323 historiaDino.forEach((wpis, indeks) => {
324 console.log(`[${indeks + 1}] ${wpis.user}: ${new Date(wpis.startTime).toLocaleTimeString()} - ${wpis.endTime ? new Date(wpis.endTime).toLocaleTimeString() : 'in progress'}`);
325 });
326}
327
328// Run the test
329testResourceTracking();In this example, we created a decorator factory for tracking resource usage in Jurassic Park. The
TrackedResourceUsage factory creates decorators that automatically register the start and end of resource usage, and can also check its availability. Based on it, we created simplified decorators for frequently used resource types, such as TrackedDinosaurUsage, TrackedEnclosureUsage, TrackedVehicleUsage, and TrackedEquipmentUsage.One of the greatest advantages of decorator factories is the ability to combine them, which allows creating more complex and flexible solutions.
1// Combining decorator factories
2function FullTracking(typZasobu: TypZasobu, poziomLogowania: PoziomLogowania = "Info") {
3 return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
4 // First apply logging
5 LogowanieSystemu({
6 podsystem: typZasobu === "dinosaur" ? "Dinosaury" :
7 typZasobu === "zagroda" ? "Zagrody" :
8 typZasobu === "pojazd" ? "Transport" :
9 typZasobu === "equipment" ? "Equipment" : "System",
10 poziom: poziomLogowania,
11 czasWykonania: true
12 })(target, propertyKey, descriptor);
13
14 // Then apply resource tracking
15 return TrackedResourceUsage({
16 typZasobu,
17 checkAccess: typZasobu === "pojazd" || typZasobu === "equipment"
18 })(target, propertyKey, descriptor);
19 };
20}
21
22// Example of using combined factories
23class ParkManagementV2 {
24 user: string;
25 securityContext?: SecurityContext;
26
27 constructor(user: string, permissionLevel: PermissionLevel) {
28 this.user = user;
29 this.securityContext = { user, permissionLevel };
30 }
31
32 // Combining resource tracking, logging, and permissions
33 @ForEmployees()
34 @FullTracking("dinosaur", "Info")
35 takeCareOfDinosaur(idDinosaura: string): boolean {
36 console.log(`Taking care of dinosaur ${idDinosaura}`);
37 return true;
38 }
39
40 // Combining resource tracking, logging, and permissions with a higher level
41 @ForManagers()
42 @FullTracking("zagroda", "Warning")
43 modyfikujUstawieniaZagrody(idZagrody: string, ustawienia: any): boolean {
44 console.log(`Modifying enclosure settings ${idZagrody}`);
45 return true;
46 }
47
48 // Combining all necessary decorators for a critical function
49 @ForAdministrators()
50 @FullTracking("equipment", "Krytyczny")
51 configureSecuritySystems(equipmentId: string, parametery: any): boolean {
52 console.log(`Configuring security systems using ${equipmentId}`);
53 return true;
54 }
55}
56
57// Test combining decorator factories
58function testFactoryChaining() {
59 console.log("=== Test: Combining decorator factories ===");
60
61 const pracownik = new ParkManagementV2("Mary", PermissionLevel.Pracownik);
62 const kierownik = new ParkManagementV2("Bob", PermissionLevel.Kierownik);
63 const admin = new ParkManagementV2("Alice", PermissionLevel.Administrator);
64
65 // Test employee
66 console.log("\n--- Test: Employee ---");
67 try {
68 pracownik.takeCareOfDinosaur("DINO-002"); // Should work
69 pracownik.modyfikujUstawieniaZagrody("ZAG-002", { temperatura: 30 }); // Should throw error
70 } catch (error) {
71 console.error(error.message);
72 }
73
74 // Test manager
75 console.log("\n--- Test: Manager ---");
76 try {
77 kierownik.modyfikujUstawieniaZagrody("ZAG-002", { temperatura: 30 }); // Should work
78 kierownik.configureSecuritySystems("SEC-001", { poziom: "high" }); // Should throw error
79 } catch (error) {
80 console.error(error.message);
81 }
82
83 // Test administrator
84 console.log("\n--- Test: Administrator ---");
85 try {
86 admin.configureSecuritySystems("SEC-001", { poziom: "high" }); // Should work
87 } catch (error) {
88 console.error(error.message);
89 }
90}
91
92// Run the test
93testFactoryChaining();In this example, we show how different decorator factories can be combined to create comprehensive solutions. The
FullTracking function combines logging and resource tracking decorators, and is then used together with permission decorators.Make sure all decorator factories have a similar structure and naming convention. This way, the code will be more readable and predictable.
1// Consistent decorator factory structure
2function LogowanieDanych(options: OpcjeLogowania) { /* ... */ }
3function WalidacjaDanych(options: OpcjeWalidacji) { /* ... */ }
4function DataTracking(options: TrackingOptions) { /* ... */ }Always specify precise types for factory parameters to ensure type safety and IDE support.
1// Good typing
2interface OpcjeWalidacji<T> {
3 walidator: (value: T) => boolean;
4 komunikat: string;
5}
6
7function Waliduj<T>(options: OpcjeWalidacji<T>) {
8 //
9}Decorator factories should have reasonable default values for non-critical options.
1function LogOperation(options: Partial<OpcjeLogowania> = {}) {
2 const {
3 poziom = "Info",
4 detailed = false,
5 czasWykonania = false
6 } = options;
7
8 //
9}For frequently used configurations, create specialized factories that make using decorators easier.
1// Specialized versions for different logging levels
2function LogujDebug(detailed: boolean = false) {
3 return LogOperation({ poziom: "Debug", detailed });
4}
5
6function LogujInfo() {
7 return LogOperation({ poziom: "Info" });
8}
9
10function LogWarning() {
11 return LogOperation({ poziom: "Warning", czasWykonania: true });
12}
13
14function LogError() {
15 return LogOperation({ poziom: "Error", detailed: true, czasWykonania: true });
16}Well-documented decorator factories make them easier to use.
1/**
2 * Creates a decorator that validates a method parameter.
3 *
4 * @param options - Validation options
5 * @param options.walidator - Function that checks value correctness
6 * @param options.komunikat - Error message for invalid values
7 * @param options.transformacja - Optional function to transform the value
8 *
9 * @returns A method parameter decorator
10 *
11 * @example
12 * class Example {
13 * metoda(@Waliduj({ walidator: (x) => x > 0, komunikat: "Musi be dodatnia" }) count: number) {
14 * //
15 * }
16 * }
17 */
18function Waliduj<T>(options: OpcjeWalidacji<T>) {
19 //
20}Decorator factories should be explicit about their dependencies.
1// Bad - hidden dependency
2function LogujDoPliku(pathPliku: string) {
3 // Directly uses the file system
4 const fs = require('fs');
5 //
6}
7
8// Good - explicit dependency
9function LogujDoPliku(logger: FileLogger) {
10 // Logger is passed as a parameter
11 //
12}Write unit tests for your decorator factories to make sure they work as expected.
1describe('WalidujZakres', () => {
2 it('should pass a value within range', () => {
3 //
4 });
5
6 it('should throw an error for a value out of range', () => {
7 //
8 });
9
10 it('should clamp the value to range when transformation is enabled', () => {
11 //
12 });
13});Decorator factories are particularly useful in the following cases:
Decorator factories in TypeScript are a powerful tool that allows creating flexible, configurable, and reusable decorators. Just as systems in Jurassic Park must be adapted to different needs and requirements while maintaining uniform safety standards, decorator factories enable creating consistent yet case-specific decorators.
Thanks to decorator factories, we can:
Decorator factories are an advanced TypeScript technique that requires a good understanding of decorators, but they offer enormous benefits in terms of code organization, reuse, and readability. When well designed, they become incredibly valuable tools in your programming arsenal.