"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.
Class decorators allow modifying or extending classes in a declarative way. They are especially useful when we want to:
A class decorator is simply a function that takes a class constructor as an argument and can modify, extend, or completely replace it.
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}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.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.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:@Rejestrowany - registers the class in the system@PoziomBezpieczenstwa(3) - sets the security level to 3@Logowany("[Gate control system]") - adds logging to all class methodsBelow 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:
@RegisterResource - automatically registers resources in a central registry@WymagaAudytu - adds audit schedule tracking functionality@TrackStatuses - adds status change history@Singleton - ensures only one instance of a class can be createdClass decorators are especially useful:
Class decorators also have certain limitations and issues:
as any to avoid TypeScript issuesClass 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.