We use cookies to enhance your experience on the site
CodeWorlds

Class Decorators

"In Jurassic Park, every building and piece of infrastructure must be properly labeled and secured," explains John Hammond, walking through the newly built Visitor Center. "Some areas are accessible to everyone, others only to personnel with appropriate clearances, and still others require special security protocols. Without a proper system of labels and safeguards, there would be chaos."

In TypeScript, just like in Jurassic Park, we need a way to label and modify classes without interfering with their internal implementation. Class decorators are like special labels and security protocols that we can apply to classes to add extra functionality or characteristics.

Introduction to class decorators

Class decorators allow modifying or extending classes in a declarative way. They are especially useful when we want to:

  1. Add metadata to a class
  2. Automatically register a class in a registry
  3. Add methods or properties to a class
  4. Change constructor behavior
  5. Apply design patterns like Singleton or Factory

A class decorator is simply a function that takes a class constructor as an argument and can modify, extend, or completely replace it.

Basic class decorator syntax

Class decorators are placed above the class definition, preceded by the

@
symbol:

1@dekorator
2class MojaKlasa {
3  // class implementation
4}

Where

dekorator
is a function:

1function dekorator(target: Function) {
2  // Modification or extension of the class
3}

Example: Monitoring system in Jurassic Park

Let's start with a simple class decorator that will mark objects requiring monitoring in Jurassic Park:

1// Class decorator
2function Monitorowany(target: Function) {
3  // Add a property to the class prototype
4  target.prototype.monitorowany = true;
5
6  // Add a method to the class prototype
7  target.prototype.rozpocznijMonitoring = function() {
8    console.log(`Monitoring started: ${this.nazwa || this.constructor.name}`);
9  };
10}
11
12// Using the decorator
13@Monitorowany
14class ZagrodaDinosaurow {
15  nazwa: string;
16
17  constructor(nazwa: string) {
18    this.nazwa = nazwa;
19  }
20
21  dodajDinosaura(dinosaur: string) {
22    console.log(`Added ${dinosaur} to enclosure ${this.nazwa}`);
23  }
24}
25
26// Using the class with the decorator
27const raptorEnclosure = new ZagrodaDinosaurow("Velociraptor");
28raptorEnclosure.dodajDinosaura("Blue");
29
30// Now we can use the properties and methods added by the decorator
31if ((raptorEnclosure as any).monitorowany) {
32  (raptorEnclosure as any).rozpocznijMonitoring();
33}

In this example, the

Monitorowany
decorator adds a
monitorowany
property and a
rozpocznijMonitoring
method to the prototype of the
ZagrodaDinosaurow
class. Thanks to this, all instances of this class will have access to these additional features.

Class decorators with parameters

We can also create decorators that accept parameters. In this case, our decorator function must return another function, which will be the actual decorator:

1// Decorator factory
2function PoziomBezpieczenstwa(poziom: number) {
3  // We return the actual class decorator
4  return function(target: Function) {
5    // Add a property to the class (not to the prototype)
6    target.prototype.poziomBezpieczenstwa = poziom;
7
8    // Add an access verification method
9    target.prototype.sprawdzDostep = function(poziomPracownika: number): boolean {
10      return poziomPracownika >= poziom;
11    };
12
13    // We can also override the constructor
14    const oryginalnyConstructor = target;
15
16    // Function replacing the constructor
17    function nowyConstructor(...args: any[]) {
18      console.log(`Creating object with security level: ${poziom}`);
19
20      // Call the original constructor
21      const instancja = new (oryginalnyConstructor as any)(...args);
22
23      // Additional logic can be added
24      if (poziom >= 4) {
25        console.log("WARNING: Creating high security level object!");
26      }
27
28      return instancja;
29    }
30
31    // Copy prototype properties
32    nowyConstructor.prototype = oryginalnyConstructor.prototype;
33
34    // Return the new constructor
35    return nowyConstructor as any;
36  };
37}
38
39// Using the decorator with a parameter
40@PoziomBezpieczenstwa(4)
41class LaboratoriumGenetyczne {
42  nazwa: string;
43
44  constructor(nazwa: string) {
45    this.nazwa = nazwa;
46    console.log(`Laboratory created: ${nazwa}`);
47  }
48
49  klonujDinosaura(gatunek: string) {
50    console.log(`Cloning ${gatunek} in laboratory ${this.nazwa}`);
51  }
52}
53
54// Using the class with the decorator
55const laboratorium = new LaboratoriumGenetyczne("Hammond Lab");
56
57// Checking the security level
58console.log(`Security level: ${(laboratorium as any).poziomBezpieczenstwa}`);
59
60// Verifying access
61const poziomPracownika = 3;
62if ((laboratorium as any).sprawdzDostep(poziomPracownika)) {
63  console.log("Access granted to laboratory");
64  laboratorium.klonujDinosaura("Velociraptor");
65} else {
66  console.log("Access denied! Higher security level required.");
67}

In this example, the

PoziomBezpieczenstwa
decorator takes a parameter specifying the required access level. It adds a
poziomBezpieczenstwa
property and a
sprawdzDostep
method to the class, and also overrides the constructor to add additional logging.

Combining multiple decorators

We can apply multiple decorators to a single class. In this case, decorators are applied from bottom to top (from last to first):

1// Decorator that registers in the system
2function Rejestrowany(target: Function) {
3  console.log(`Registering class: ${target.name}`);
4  (target as any).zarejestrowany = true;
5}
6
7// Decorator that adds logging
8function Logowany(prefiks: string) {
9  return function(target: Function) {
10    // Save original methods
11    const oryginalneMetody: Record<string, Function> = {};
12
13    // Iterate over prototype methods
14    for (const propertyName of Object.getOwnPropertyNames(target.prototype)) {
15      const descriptor = Object.getOwnPropertyDescriptor(target.prototype, propertyName);
16
17      // Check if it's a method and not a constructor
18      if (descriptor && typeof descriptor.value === 'function' && propertyName !== 'constructor') {
19        oryginalneMetody[propertyName] = descriptor.value;
20
21        // Override the method, adding logging
22        Object.defineProperty(target.prototype, propertyName, {
23          value: function(...args: any[]) {
24            console.log(`${prefiks}: Calling ${propertyName} with arguments: ${JSON.stringify(args)}`);
25            const rezultat = oryginalneMetody[propertyName].apply(this, args);
26            console.log(`${prefiks}: ${propertyName} returned: ${JSON.stringify(rezultat)}`);
27            return rezultat;
28          }
29        });
30      }
31    }
32  };
33}
34
35// Using multiple decorators
36@Rejestrowany
37@PoziomBezpieczenstwa(3)
38@Logowany("[Gate control system]")
39class SystemKontroliBram {
40  stan: "otwarte" | "closed" = "closed";
41
42  constructor() {
43    console.log("Initializing gate control system.");
44  }
45
46  openGates(): boolean {
47    console.log("Opening park gates...");
48    this.stan = "otwarte";
49    return true;
50  }
51
52  zamknijBramy(): boolean {
53    console.log("Closing park gates...");
54    this.stan = "closed";
55    return true;
56  }
57
58  checkStatus(): string {
59    return `Gates are ${this.stan}`;
60  }
61}
62
63// Using the class with multiple decorators
64const kontrolaBram = new SystemKontroliBram();
65kontrolaBram.openGates();
66console.log(kontrolaBram.checkStatus());
67kontrolaBram.zamknijBramy();

In this example, the

SystemKontroliBram
class has three decorators applied:

  1. @Rejestrowany
    - registers the class in the system
  2. @PoziomBezpieczenstwa(3)
    - sets the security level to 3
  3. @Logowany("[Gate control system]")
    - adds logging to all class methods

Practical example: Park management system

Below is a more elaborate example of a Jurassic Park management system using various class decorators:

1// Basic interface for all park resources
2interface ZasobParku {
3  id: string;
4  nazwa: string;
5  location: string;
6  status: "aktywny" | "nieaktywny" | "w konserwacji" | "awaria";
7}
8
9// Park resource registry
10class ResourceRegistry {
11  private static rejestr: Map<string, ZasobParku[]> = new Map();
12
13  static registerResource(typ: string, resource: ZasobParku): void {
14    if (!this.rejestr.has(typ)) {
15      this.rejestr.set(typ, []);
16    }
17
18    this.rejestr.get(typ)!.push(resource);
19    console.log(`Resource registered: ${resource.nazwa} (${typ})`);
20  }
21
22  static pobierzZasoby(typ: string): ZasobParku[] {
23    return this.rejestr.get(typ) || [];
24  }
25
26  static pobierzWszystkieZasoby(): Map<string, ZasobParku[]> {
27    return new Map(this.rejestr);
28  }
29}
30
31// Decorator for resource registration
32function RegisterResource(typ: string) {
33  return function<T extends { new(...args: any[]): ZasobParku }>(konstruktor: T) {
34    return class extends konstruktor {
35      constructor(...args: any[]) {
36        super(...args);
37        ResourceRegistry.registerResource(typ, this);
38      }
39    };
40  };
41}
42
43// Decorator enforcing audits
44function WymagaAudytu(frequencyDays: number) {
45  return function(target: Function) {
46    target.prototype.ostatniAudyt = new Date();
47    target.prototype.auditFrequency = frequencyDays;
48
49    target.prototype.wymagaAudytu = function(): boolean {
50      const teraz = new Date();
51      const timeDifference = teraz.getTime() - this.ostatniAudyt.getTime();
52      const differenceDays = timeDifference / (1000 * 3600 * 24);
53
54      return differenceDays >= this.auditFrequency;
55    };
56
57    target.prototype.performAudit = function(): void {
58      console.log(`Audit conducted for: ${this.nazwa}`);
59      this.ostatniAudyt = new Date();
60    };
61  };
62}
63
64// Decorator adding status history tracking
65function TrackStatuses(target: Function) {
66  // Add status history array
67  target.prototype.statusHistory = [];
68
69  // Save original status setter
70  const oryginalnySetStatus = Object.getOwnPropertyDescriptor(
71    target.prototype, 'status'
72  )?.set;
73
74  // Override the setter for status
75  if (oryginalnySetStatus) {
76    Object.defineProperty(target.prototype, 'status', {
77      set: function(nowyStatus: "aktywny" | "nieaktywny" | "w konserwacji" | "awaria") {
78        const staryCzas = new Date();
79        const staryStatus = this._status;
80
81        // Call the original setter
82        oryginalnySetStatus.call(this, nowyStatus);
83
84        // Save the change in history
85        this.statusHistory.push({
86          czasZmiany: staryCzas,
87          z: staryStatus,
88          na: nowyStatus
89        });
90
91        console.log(`Status changed for ${this.nazwa}: ${staryStatus || 'none'} -> ${nowyStatus}`);
92      },
93      get: Object.getOwnPropertyDescriptor(target.prototype, 'status')?.get
94    });
95  }
96}
97
98// Singleton decorator
99function Singleton<T extends { new(...args: any[]): any }>(konstruktor: T) {
100  // Save original constructor
101  const oryginalnyConstructor = konstruktor;
102
103  // Replace constructor with a function that controls instance creation
104  const nowyConstructor: any = function(...args: any[]) {
105    if (!nowyConstructor.instancja) {
106      nowyConstructor.instancja = new oryginalnyConstructor(...args);
107    } else {
108      console.warn(`Singleton ${konstruktor.name} already exists. Returning existing instance.`);
109    }
110
111    return nowyConstructor.instancja;
112  };
113
114  // Copy prototype
115  nowyConstructor.prototype = oryginalnyConstructor.prototype;
116
117  // Store original constructor
118  nowyConstructor.oryginalnyConstructor = oryginalnyConstructor;
119
120  return nowyConstructor;
121}
122
123// Implementation of various park resources
124
125@RegisterResource("zagroda")
126@WymagaAudytu(7) // Audit every 7 days
127@TrackStatuses
128class ZagrodaDinosaurs implements ZasobParku {
129  id: string;
130  nazwa: string;
131  location: string;
132  private _status: "aktywny" | "nieaktywny" | "w konserwacji" | "awaria" = "aktywny";
133
134  // Enclosure-specific properties
135  genusDinosaurs: string;
136  capacity: number;
137  currentLiczba: number;
138
139  constructor(
140    id: string,
141    nazwa: string,
142    location: string,
143    genusDinosaurs: string,
144    capacity: number
145  ) {
146    this.id = id;
147    this.nazwa = nazwa;
148    this.location = location;
149    this.genusDinosaurs = genusDinosaurs;
150    this.capacity = capacity;
151    this.currentLiczba = 0;
152  }
153
154  get status(): "aktywny" | "nieaktywny" | "w konserwacji" | "awaria" {
155    return this._status;
156  }
157
158  set status(nowyStatus: "aktywny" | "nieaktywny" | "w konserwacji" | "awaria") {
159    this._status = newStatus;
160  }
161
162  dodajDinosaura(): boolean {
163    if (this.currentLiczba < this.capacity && this.status === "aktywny") {
164      this.currentLiczba++;
165      console.log(`Dinosaur added to enclosure ${this.nazwa}. Current count: ${this.currentLiczba}`);
166      return true;
167    } else {
168      console.log(`Cannot add dinosaur to enclosure ${this.nazwa}. Enclosure full or inactive.`);
169      return false;
170    }
171  }
172
173  removeDinosaur(): boolean {
174    if (this.currentLiczba > 0) {
175      this.currentLiczba--;
176      console.log(`Dinosaur removed from enclosure ${this.nazwa}. Current count: ${this.currentLiczba}`);
177      return true;
178    } else {
179      console.log(`Cannot remove dinosaur from enclosure ${this.nazwa}. Enclosure empty.`);
180      return false;
181    }
182  }
183}
184
185@RegisterResource("budynek")
186@WymagaAudytu(30) // Audit every 30 days
187@TrackStatuses
188class BudynekParku implements ZasobParku {
189  id: string;
190  nazwa: string;
191  location: string;
192  private _status: "aktywny" | "nieaktywny" | "w konserwacji" | "awaria" = "aktywny";
193
194  // Building-specific properties
195  genusBudynku: "publiczny" | "administracja" | "laboratorium" | "utrzymanie";
196  powierzchnia: number;
197
198  constructor(
199    id: string,
200    nazwa: string,
201    location: string,
202    genusBudynku: "publiczny" | "administracja" | "laboratorium" | "utrzymanie",
203    powierzchnia: number
204  ) {
205    this.id = id;
206    this.nazwa = nazwa;
207    this.location = location;
208    this.genusBudynku = genusBudynku;
209    this.powierzchnia = powierzchnia;
210  }
211
212  get status(): "aktywny" | "nieaktywny" | "w konserwacji" | "awaria" {
213    return this._status;
214  }
215
216  set status(nowyStatus: "aktywny" | "nieaktywny" | "w konserwacji" | "awaria") {
217    this._status = newStatus;
218  }
219
220  open(): void {
221    if (this.status !== "aktywny") {
222      console.log(`Cannot open building ${this.nazwa}. Status: ${this.status}`);
223      return;
224    }
225
226    console.log(`Building ${this.nazwa} opened`);
227  }
228
229  zamknij(): void {
230    console.log(`Building ${this.nazwa} closed`);
231  }
232}
233
234@Singleton
235@RegisterResource("system")
236class SecuritySystem implements ZasobParku {
237  id: string;
238  nazwa: string;
239  location: string;
240  private _status: "aktywny" | "nieaktywny" | "w konserwacji" | "awaria" = "aktywny";
241
242  // Security system-specific properties
243  emergencyProtocol: "normall" | "warning" | "threat" | "ewakuacja" = "normall";
244
245  constructor() {
246    this.id = "SYS-SEC-001";
247    this.nazwa = "Main Security System";
248    this.location = "Control center";
249  }
250
251  get status(): "aktywny" | "nieaktywny" | "w konserwacji" | "awaria" {
252    return this._status;
253  }
254
255  set status(nowyStatus: "aktywny" | "nieaktywny" | "w konserwacji" | "awaria") {
256    this._status = newStatus;
257
258    if (nowyStatus !== "aktywny" && this.emergencyProtocol === "normall") {
259      console.warn("WARNING: Security system is not active!");
260      this.setProtocol("warning");
261    }
262  }
263
264  setProtocol(protocol: "normall" | "warning" | "threat" | "ewakuacja"): void {
265    this.emergencyProtocol = protocol;
266    console.log(`Security protocol set to: ${protocol}`);
267
268    // Action implementation depending on the protocol
269    switch (protocol) {
270      case "normall":
271        console.log("All systems operating normallly.");
272        break;
273      case "warning":
274        console.log("Increased staff alertness. Checking all systems.");
275        break;
276      case "threat":
277        console.log("WARNING: Threat detected! Staff to positions! Prepare for evacuation.");
278        break;
279      case "ewakuacja":
280        console.log("WARNING! WARNING! Immediate park evacuation! This is not a drill!");
281        break;
282    }
283  }
284
285  checkStatus(): void {
286    console.log(`Security system status: ${this.status}`);
287    console.log(`Current protocol: ${this.emergencyProtocol}`);
288  }
289}
290
291// Using classes with decorators
292
293// Creating an enclosure
294const zagrodaT = new ZagrodaDinosaurs(
295  "ZAG-001",
296  "T-Rex Enclosure",
297  "Sector A",
298  "Tyrannosaurus Rex",
299  2
300);
301
302zagrodaT.dodajDinosaura();
303zagrodaT.status = "w konserwacji";
304zagrodaT.status = "aktywny";
305
306// Creating a building
307const visitorCenter = new BudynekParku(
308  "BUD-001",
309  "Visitor Center",
310  "Main entrance",
311  "publiczny",
312  2500
313);
314
315visitorCenter.open();
316visitorCenter.status = "w konserwacji";
317visitorCenter.status = "aktywny";
318
319// Creating the security system (Singleton)
320const securitySystem1 = new SecuritySystem();
321securitySystem1.checkStatus();
322securitySystem1.setProtocol("warning");
323
324// Attempting to create a second security system
325const securitySystem2 = new SecuritySystem();
326// Note that securitySystem1 === securitySystem2
327
328// Checking audits
329console.log(`T-Rex Enclosure requires audit: ${(zagrodaT as any).wymagaAudytu()}`);
330console.log(`Visitor Center requires audit: ${(visitorCenter as any).wymagaAudytu()}`);
331
332// Conducting audit
333if ((zagrodaT as any).wymagaAudytu()) {
334  (zagrodaT as any).performAudit();
335}
336
337// Getting all resources from registry
338console.log("Park resource list:");
339const wszystkieZasoby = ResourceRegistry.pobierzWszystkieZasoby();
340for (const [typ, zasoby] of wszystkieZasoby) {
341  console.log(`Type: ${typ}, Count: ${zasoby.length}`);
342  zasoby.forEach(resource => {
343    console.log(`  - ${resource.nazwa} (${resource.status})`);
344  });
345}
346
347// Checking status history
348console.log("T-Rex Enclosure status history:");
349(zagrodaT as any).statusHistory.forEach((zmiana: any) => {
350  console.log(`  ${zmiana.czasZmiany.toLocaleString()}: ${zmiana.z || 'initial'} -> ${zmiana.na}`);
351});

This elaborate example demonstrates advanced use of class decorators:

  1. @RegisterResource
    - automatically registers resources in a central registry
  2. @WymagaAudytu
    - adds audit schedule tracking functionality
  3. @TrackStatuses
    - adds status change history
  4. @Singleton
    - ensures only one instance of a class can be created

When to use class decorators

Class decorators are especially useful:

  1. When you want to add cross-cutting functionality - when the same functionality needs to be added to many classes (so-called aspect-oriented programming)
  2. When implementing design patterns - such as Singleton, Factory, Observer
  3. For automatic registration - e.g., registering components in a dependency injection system
  4. For metaprogramming - when code should modify itself at runtime
  5. For adding metadata - which can later be used by a framework

Limitations and issues with class decorators

Class decorators also have certain limitations and issues:

  1. Harder debugging - code with many decorators can be more difficult to debug
  2. Typing problems - sometimes you need to use
    as any
    to avoid TypeScript issues
  3. Hidden functionality - decorators can hide important functionality, making code harder to understand
  4. Application order - the order of applying decorators matters and can cause problems
  5. Specification changes - the decorator specification in JavaScript and TypeScript is still evolving

Summary

Class decorators in TypeScript are a powerful tool that allows adding, modifying, or replacing class functionality in a declarative way, without interfering with their internal implementation. Just as special security protocols in Jurassic Park help maintain order and control, class decorators help organize and extend code in a clear and systematic manner.

Class decorators are especially useful in large applications where cross-cutting functionality often needs to be implemented across many classes, such as logging, authorization, validation, or component registration. Thanks to them, we can create more modular, flexible, and maintainable code.

However, remember that like all powerful tools, class decorators should be used wisely. Overusing them can lead to code that is difficult to debug and understand. Always use them when they genuinely solve a problem, not just because they look elegant.

Go to CodeWorlds