JavaScript and TypeScript course Β· Module 7: Advanced TypeScript
Class Decorators
In this lesson9
"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:
- Add metadata to a class
- Automatically register a class in a registry
- Add methods or properties to a class
- Change constructor behavior
- 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:
@Rejestrowany- registers the class in the system@PoziomBezpieczenstwa(3)- sets the security level to 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:
@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 created
When to use class decorators
Class decorators are especially useful:
- When you want to add cross-cutting functionality - when the same functionality needs to be added to many classes (so-called aspect-oriented programming)
- When implementing design patterns - such as Singleton, Factory, Observer
- For automatic registration - e.g., registering components in a dependency injection system
- For metaprogramming - when code should modify itself at runtime
- 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:
- Harder debugging - code with many decorators can be more difficult to debug
- Typing problems - sometimes you need to use
as anyto avoid TypeScript issues - Hidden functionality - decorators can hide important functionality, making code harder to understand
- Application order - the order of applying decorators matters and can cause problems
- 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.
Code for this lesson: index.ts
1// A complete park type system - Jurassic Park
2console.log("=== ADVANCED PARK TYPE SYSTEM ===\n");
3
4// ===========================================
5// 1. Base types and interfaces
6// ===========================================
7
8interface Entity {
9 readonly id: string;
10 readonly createdAt: Date;
11}
12
13interface Dinosaur extends Entity {
14 name: string;
15 species: string;
16 weight: number;
17 diet: "carnivore" | "herbivore" | "omnivore";
18 dangerLevel: number;
19}
20
21interface Enclosure extends Entity {
22 name: string;
23 capacity: number;
24 securityLevel: number;
25 residents: string[];
26}
27
28interface StaffMember extends Entity {
29 name: string;
30 role: "ranger" | "veterinarian" | "researcher" | "security";
31 clearanceLevel: number;
32}
33
34// ===========================================
35// 2. Mapped and conditional types
36// ===========================================
37
38type EntityMap = {
39 dinosaur: Dinosaur;
40 enclosure: Enclosure;
41 staff: StaffMember;
42};
43
44type EntityType = keyof EntityMap;
45
46// Creating DTO types automatically
47type CreateDTO<T extends Entity> = Omit<T, "id" | "createdAt">;
48type UpdateDTO<T extends Entity> = Partial<Omit<T, "id" | "createdAt">>;
49type ReadDTO<T extends Entity> = Readonly<T>;
50
51// ===========================================
52// 3. A generic repository with constraints
53// ===========================================
54
55class Repository<T extends Entity> {
56 private storage: Map<string, T> = new Map();
57 private counter = 0;
58 private prefix: string;
59
60 constructor(prefix: string) {
61 this.prefix = prefix;
62 }
63
64 create(data: CreateDTO<T>): T {
65 const id = `${this.prefix}-${String(++this.counter).padStart(3, "0")}`;
66 const entity = { ...data, id, createdAt: new Date() } as T;
67 this.storage.set(id, entity);
68 return entity;
69 }
70
71 findById(id: string): ReadDTO<T> | undefined {
72 return this.storage.get(id);
73 }
74
75 findAll(): ReadDTO<T>[] {
76 return Array.from(this.storage.values());
77 }
78
79 update(id: string, data: UpdateDTO<T>): T | undefined {
80 const existing = this.storage.get(id);
81 if (!existing) return undefined;
82 const updated = { ...existing, ...data } as T;
83 this.storage.set(id, updated);
84 return updated;
85 }
86
87 delete(id: string): boolean {
88 return this.storage.delete(id);
89 }
90
91 query(predicate: (item: T) => boolean): ReadDTO<T>[] {
92 return this.findAll().filter(item => predicate(item as T));
93 }
94}
95
96// ===========================================
97// 4. An event system with conditional types
98// ===========================================
99
100type EventSeverity = "info" | "warning" | "critical";
101
102type ParkEvent<S extends EventSeverity> = S extends "critical"
103 ? { severity: S; message: string; requiresEvacuation: boolean; affectedZones: string[] }
104 : S extends "warning"
105 ? { severity: S; message: string; affectedZones: string[] }
106 : { severity: S; message: string };
107
108function createEvent<S extends EventSeverity>(
109 severity: S,
110 data: Omit<ParkEvent<S>, "severity">
111): ParkEvent<S> {
112 return { severity, ...data } as ParkEvent<S>;
113}
114
115// ===========================================
116// 5. A report system with mapped types
117// ===========================================
118
119type DinoReport = {
120 [K in keyof Omit<Dinosaur, "id" | "createdAt">]: {
121 label: string;
122 value: Dinosaur[K];
123 formatted: string;
124 };
125};
126
127function generateReport(dino: Dinosaur): DinoReport {
128 return {
129 name: { label: "Name", value: dino.name, formatted: dino.name },
130 species: { label: "Species", value: dino.species, formatted: dino.species },
131 weight: { label: "Weight", value: dino.weight, formatted: `${dino.weight} kg` },
132 diet: { label: "Diet", value: dino.diet, formatted: dino.diet === "carnivore" ? "Carnivore" : dino.diet === "herbivore" ? "Herbivore" : "Omnivore" },
133 dangerLevel: { label: "Danger", value: dino.dangerLevel, formatted: `${dino.dangerLevel}/10` }
134 };
135}
136
137// ===========================================
138// Test
139// ===========================================
140
141// Repositories
142const dinoRepo = new Repository<Dinosaur>("DINO");
143const encRepo = new Repository<Enclosure>("ENC");
144const staffRepo = new Repository<StaffMember>("STAFF");
145
146// Creating dinosaurs
147console.log("--- Creating entities ---");
148const rex = dinoRepo.create({ name: "Rexy", species: "T-Rex", weight: 8000, diet: "carnivore", dangerLevel: 10 });
149const blue = dinoRepo.create({ name: "Blue", species: "Velociraptor", weight: 15, diet: "carnivore", dangerLevel: 8 });
150const patty = dinoRepo.create({ name: "Patty", species: "Brachiosaurus", weight: 50000, diet: "herbivore", dangerLevel: 1 });
151
152const mainEnc = encRepo.create({ name: "T-Rex Zone", capacity: 2, securityLevel: 10, residents: [rex.id] });
153const grant = staffRepo.create({ name: "Alan Grant", role: "researcher", clearanceLevel: 8 });
154
155console.log(`Dinosaur: ${rex.id} - ${rex.name}`);
156console.log(`Enclosure: ${mainEnc.id} - ${mainEnc.name}`);
157console.log(`Staff member: ${grant.id} - ${grant.name}`);
158
159// Update
160console.log("\n--- Update ---");
161const updatedRex = dinoRepo.update(rex.id, { weight: 8200 });
162console.log(`${updatedRex?.name}: ${updatedRex?.weight}kg`);
163
164// Query
165console.log("\n--- Queries ---");
166const dangerousDinos = dinoRepo.query(d => d.dangerLevel >= 7);
167console.log("Dangerous:", dangerousDinos.map(d => d.name).join(", "));
168
169const carnivores = dinoRepo.query(d => d.diet === "carnivore");
170console.log("Carnivores:", carnivores.map(d => d.name).join(", "));
171
172// Events
173console.log("\n--- Events ---");
174const info = createEvent("info", { message: "Routine inspection completed" });
175console.log("Info:", info.message);
176
177const warning = createEvent("warning", {
178 message: "Fence needs repair",
179 affectedZones: ["Zone A"]
180});
181console.log("Warning:", warning.message, "Zones:", warning.affectedZones.join(", "));
182
183const critical = createEvent("critical", {
184 message: "Dinosaur escape!",
185 requiresEvacuation: true,
186 affectedZones: ["Zone A", "Zone B"]
187});
188console.log("CRITICAL:", critical.message, "Evacuation:", critical.requiresEvacuation);
189
190// Report
191console.log("\n--- Dinosaur report ---");
192const report = generateReport(rex);
193Object.values(report).forEach(field => {
194 console.log(` ${field.label}: ${field.formatted}`);
195});
196
197console.log("\nA complete TypeScript type system in action!");Spotted a mistake in this lesson?
Check yourself
Answer the questions from this lesson. Pick an answer to see right away whether it is correct.
1. Which utility type is used to EXCLUDE specific types from a union type?
2. What is a decorator in TypeScript?
Hands-on tasks in the game
- Vertical ordering
Order the steps for creating a custom utility type in TypeScript:
- Click in order
Arrange the elements of a class decorator declaration: