We use cookies to enhance your experience on the site
CodeWorlds

Decorator Factories

"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.

Introduction to Decorator Factories

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:

  1. Reuse - we can create many different decorators from a single factory
  2. Configurability - we adapt the decorator's behavior to specific needs
  3. Consistency - all decorators from one factory share the same structure and concept
  4. Easier maintenance - a change in the factory updates all decorators created from it
  5. Typing - we can define factory parameters with precise types

Basic Decorator Factory Structure

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}

Decorator Factory Examples in Jurassic Park

Let's see how we can use decorator factories in the context of managing Jurassic Park.

Example 1: Logging Decorator Factory for Different Subsystems

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.

Example 2: Data Validation Decorator Factory

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.

Example 3: Permission Decorator Factory

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
.

Example 4: Resource Management Decorator Factory

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
.

Combining Decorator Factories

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.

Best Practices for Creating Decorator Factories

1. Maintain Structural Consistency

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) { /* ... */ }

2. Use Appropriate Types for Parameters

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}

3. Provide Sensible Default Values

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}

4. Create Simplified Versions for Common Use Cases

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}

5. Document Your Factories

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}

6. Avoid Hidden Dependencies

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}

7. Test Your Decorator Factories

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});

Use Cases for Decorator Factories

Decorator factories are particularly useful in the following cases:

  1. Frameworks and libraries - creating configurable decorators for framework users
  2. Cross-cutting concerns - logging, validation, security that must be adapted to many cases
  3. Environment configuration - adapting decorator behavior to different environments (development, production)
  4. Resource management - monitoring, tracking, and controlling access to resources
  5. Metrics collection - configuring how metrics are collected based on different parameters
  6. Access control - creating permission decorators for different levels and roles

Summary

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:

  1. Improve code reuse - by creating configurable decorator templates
  2. Increase readability - by hiding complex logic behind a simple, declarative interface
  3. Ensure consistency - by applying the same patterns and structures throughout the application
  4. Increase flexibility - by adapting decorators to different use cases

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.

Go to CodeWorlds