"Every enclosure in Jurassic Park must be adapted to a specific dinosaur species," explains Dr. Ellie Sattler, head of the botanical research department. "But despite the differences, all enclosures operate according to the same principles - entrance control, environmental condition monitoring, automated feeding systems. What distinguishes a velociraptor enclosure from a triceratops enclosure is not its structure, but its parameters."
Generic classes in TypeScript work the same way. They allow creating reusable components that can work with different data types while maintaining full type control. If generic functions showed us the basic idea of generics, generic classes demonstrate the full potential of this mechanism.
Generic classes are class templates that can work with different data types. Just like generic functions, they use type parameters (e.g.,
<T>), but they allow using those parameters throughout the entire class - in fields, methods, and constructors.Here's a simple generic class representing a dinosaur enclosure:
1class DinosaurEnclosure<T> {
2 private inhabitants: T[];
3
4 constructor() {
5 this.inhabitants = [];
6 }
7
8 addInhabitant(dino: T): void {
9 this.inhabitants.push(dino);
10 console.log("Added new inhabitant to enclosure!");
11 }
12
13 getInhabitants(): T[] {
14 return [...this.inhabitants]; // We return a copy to avoid direct modification
15 }
16
17 countInhabitants(): number {
18 return this.inhabitants.length;
19 }
20}The above class can be used to create an enclosure for any type of dinosaurs:
1// Dinosaur type definitions
2interface Velociraptor {
3 name: string;
4 speed: number;
5 clawLength: number;
6}
7
8interface Triceratops {
9 name: string;
10 hornLength: number;
11 weight: number;
12}
13
14// Creating specialized enclosures
15const raptorEnclosure = new DinosaurEnclosure<Velociraptor>();
16const triceratopsEnclosure = new DinosaurEnclosure<Triceratops>();
17
18// Adding inhabitants to enclosures
19raptorEnclosure.addInhabitant({
20 name: "Blue",
21 speed: 70,
22 clawLength: 15
23});
24
25triceratopsEnclosure.addInhabitant({
26 name: "Topsy",
27 hornLength: 80,
28 weight: 9000
29});
30
31// Checking inhabitants
32console.log(`Velociraptor count: ${raptorEnclosure.countInhabitants()}`);
33console.log(`Triceratops count: ${triceratopsEnclosure.countInhabitants()}`);
34
35// TypeScript ensures type safety
36// This won't compile:
37// raptorEnclosure.addInhabitant({
38// name: "Invalid",
39// hornLength: 50,
40// weight: 7000
41// });Generic classes can use multiple type parameters, allowing even greater flexibility:
1class ParkResearchLab<TSpecimen, TResult> {
2 private specimens: TSpecimen[] = [];
3 private results: Map<string, TResult> = new Map();
4
5 addSpecimen(id: string, specimen: TSpecimen): void {
6 this.specimens.push(specimen);
7 console.log(`Added sample #${id}`);
8 }
9
10 recordResult(specimenId: string, result: TResult): void {
11 this.results.set(specimenId, result);
12 console.log(`Saved result for sample #${specimenId}`);
13 }
14
15 getResult(specimenId: string): TResult | undefined {
16 return this.results.get(specimenId);
17 }
18}
19
20// Example usage with specific types
21interface BloodSample {
22 dinosaurId: string;
23 extractionDate: Date;
24 volume: number;
25}
26
27interface BloodTestResult {
28 oxygenLevel: number;
29 whiteBloodCellCount: number;
30 isHealthy: boolean;
31}
32
33const bloodLab = new ParkResearchLab<BloodSample, BloodTestResult>();
34
35bloodLab.addSpecimen("BLD-001", {
36 dinosaurId: "TREX-01",
37 extractionDate: new Date(2023, 4, 15),
38 volume: 250
39});
40
41bloodLab.recordResult("BLD-001", {
42 oxygenLevel: 95,
43 whiteBloodCellCount: 7500,
44 isHealthy: true
45});
46
47const result = bloodLab.getResult("BLD-001");
48console.log(`Wynik badania krwi: ${result?.isHealthy ? "Zdrowy" : "Wymaga uwagi"}`);Generic classes can also be extended, allowing for more specialized implementations:
1class MonitoredEnclosure<T> extends DinosaurEnclosure<T> {
2 private lastCheckTime: Date | null = null;
3 private securityLevel: "low" | "medium" | "high";
4
5 constructor(securityLevel: "low" | "medium" | "high" = "medium") {
6 super(); // Calling the base class constructor
7 this.securityLevel = securityLevel;
8 }
9
10 performSecurityCheck(): void {
11 this.lastCheckTime = new Date();
12 console.log(`Security check performed at ${this.lastCheckTime.toLocaleTimeString()}`);
13 console.log(`Security level: ${this.securityLevel}`);
14 console.log(`Inhabitant count: ${this.countInhabitants()}`);
15 }
16
17 getLastCheckTime(): Date | null {
18 return this.lastCheckTime;
19 }
20
21 upgradeSecurityLevel(): void {
22 if (this.securityLevel === "low") {
23 this.securityLevel = "medium";
24 } else if (this.securityLevel === "medium") {
25 this.securityLevel = "high";
26 }
27 console.log(`Updated security level to: ${this.securityLevel}`);
28 }
29}
30
31// Using the extended class
32const monitoredRaptorEnclosure = new MonitoredEnclosure<Velociraptor>("high");
33
34monitoredRaptorEnclosure.addInhabitant({
35 name: "Delta",
36 speed: 65,
37 clawLength: 14
38});
39
40monitoredRaptorEnclosure.performSecurityCheck();Just as functions can have default parameters, generic classes can have default types for their type parameters:
1// Default dinosaur interface
2interface DefaultDinosaur {
3 name: string;
4 species: string;
5 age: number;
6}
7
8// Class with a default type parameter
9class DinosaurTracker<T = DefaultDinosaur> {
10 private trackedDinos: T[] = [];
11
12 trackDinosaur(dino: T, location: string): void {
13 this.trackedDinos.push(dino);
14 console.log(`Started tracking dinosaur at location: ${location}`);
15 }
16
17 getTrackedCount(): number {
18 return this.trackedDinos.length;
19 }
20}
21
22// Usage with default type
23const defaultTracker = new DinosaurTracker();
24defaultTracker.trackDinosaur({
25 name: "Rexy",
26 species: "Tyrannosaurus Rex",
27 age: 7
28}, "Sektor B");
29
30// Usage with a custom type
31interface AquaticDinosaur {
32 name: string;
33 underwaterSpeedKmh: number;
34 maxDiveDepthMeters: number;
35}
36
37const aquaticTracker = new DinosaurTracker<AquaticDinosaur>();
38aquaticTracker.trackDinosaur({
39 name: "Nessie",
40 underwaterSpeedKmh: 40,
41 maxDiveDepthMeters: 500
42}, "Jezioro Centralne");It's important to understand that type parameters in generic classes only apply to instance properties and methods. Static properties and methods are shared between all instances of the class, regardless of their type parameters:
1class DinosaurRegistry<T> {
2 private static totalRegisteredDinos: number = 0;
3 private registeredDinos: T[] = [];
4
5 registerDinosaur(dino: T): void {
6 this.registeredDinos.push(dino);
7 DinosaurRegistry.totalRegisteredDinos++;
8 console.log(`Registered new dinosaur. Total: ${DinosaurRegistry.totalRegisteredDinos}`);
9 }
10
11 static getTotalRegisteredCount(): number {
12 return DinosaurRegistry.totalRegisteredDinos;
13 }
14}
15
16const rexRegistry = new DinosaurRegistry<Velociraptor>();
17const triRegistry = new DinosaurRegistry<Triceratops>();
18
19rexRegistry.registerDinosaur({
20 name: "Charlie",
21 speed: 68,
22 clawLength: 12
23});
24
25triRegistry.registerDinosaur({
26 name: "Cera",
27 hornLength: 75,
28 weight: 8500
29});
30
31console.log(`Total registered dinosaurs: ${DinosaurRegistry.getTotalRegisteredCount()}`); // 2In this example, the static variable
totalRegisteredDinos is shared across all instances of DinosaurRegistry, regardless of their type parameters.Generic classes often work with generic interfaces, creating a comprehensive type system:
1// Generic interface
2interface Repository<T> {
3 add(item: T): void;
4 getById(id: string): T | undefined;
5 getAll(): T[];
6 update(id: string, item: T): boolean;
7 delete(id: string): boolean;
8}
9
10// Implementation of a generic class conforming to a generic interface
11class DinosaurRepository<T extends { id: string }> implements Repository<T> {
12 private items: Map<string, T> = new Map();
13
14 add(item: T): void {
15 this.items.set(item.id, item);
16 }
17
18 getById(id: string): T | undefined {
19 return this.items.get(id);
20 }
21
22 getAll(): T[] {
23 return Array.from(this.items.values());
24 }
25
26 update(id: string, item: T): boolean {
27 if (this.items.has(id)) {
28 this.items.set(id, item);
29 return true;
30 }
31 return false;
32 }
33
34 delete(id: string): boolean {
35 return this.items.delete(id);
36 }
37}
38
39// Using the repository with a specific type
40interface DinosaurRecord {
41 id: string;
42 name: string;
43 species: string;
44 dateOfBirth: Date;
45}
46
47const dinoRepo = new DinosaurRepository<DinosaurRecord>();
48
49dinoRepo.add({
50 id: "DINO-001",
51 name: "Rex",
52 species: "Tyrannosaurus",
53 dateOfBirth: new Date(2018, 3, 10)
54});
55
56const rex = dinoRepo.getById("DINO-001");
57console.log(`Znaleziony dinosaur: ${rex?.name}, Species: ${rex?.species}`);Let's now expand our knowledge of generic classes by implementing a comprehensive asset management system for Jurassic Park:
1// Basic interface for all park assets
2interface ParkAsset {
3 id: string;
4 name: string;
5 status: "operational" | "maintenance" | "offline";
6 lastUpdated: Date;
7}
8
9// Different asset types
10interface Vehicle extends ParkAsset {
11 type: "jeep" | "boat" | "helicopter";
12 passengerCapacity: number;
13 currentFuel: number;
14}
15
16interface Facility extends ParkAsset {
17 type: "visitor" | "research" | "security";
18 location: { x: number, y: number };
19 capacity: number;
20}
21
22interface PowerSystem extends ParkAsset {
23 type: "main" | "backup" | "sector";
24 outputCapacity: number;
25 currentLoad: number;
26}
27
28// Generic interface for asset management
29interface AssetManager<T extends ParkAsset> {
30 registerAsset(asset: T): void;
31 getAsset(id: string): T | undefined;
32 updateStatus(id: string, status: T["status"]): boolean;
33 getOperationalAssets(): T[];
34}
35
36// Implementation of a generic asset management class
37class JurassicParkAssetManager<T extends ParkAsset> implements AssetManager<T> {
38 private assets: Map<string, T> = new Map();
39 private statusChangeLog: { assetId: string, oldStatus: string, newStatus: string, timestamp: Date }[] = [];
40
41 constructor(private assetType: string) {
42 console.log(`Initialized asset management system for type: ${assetType}`);
43 }
44
45 registerAsset(asset: T): void {
46 if (this.assets.has(asset.id)) {
47 throw new Error(`Asset o ID ${asset.id} already exists`);
48 }
49
50 this.assets.set(asset.id, {
51 ...asset,
52 lastUpdated: new Date()
53 });
54
55 console.log(`Zarejestrowano nowy asset: ${asset.name} (ID: ${asset.id})`);
56 }
57
58 getAsset(id: string): T | undefined {
59 return this.assets.get(id);
60 }
61
62 updateStatus(id: string, status: T["status"]): boolean {
63 const asset = this.assets.get(id);
64
65 if (!asset) {
66 console.warn(`Nie znaleziono assetu o ID ${id}`);
67 return false;
68 }
69
70 const oldStatus = asset.status;
71
72 // Status update
73 const updatedAsset = {
74 ...asset,
75 status,
76 lastUpdated: new Date()
77 };
78
79 this.assets.set(id, updatedAsset);
80
81 // Logging the change
82 this.statusChangeLog.push({
83 assetId: id,
84 oldStatus,
85 newStatus: status,
86 timestamp: new Date()
87 });
88
89 console.log(`Zaktualizowano status assetu ${asset.name} z "${oldStatus}" na "${status}"`);
90 return true;
91 }
92
93 getOperationalAssets(): T[] {
94 return Array.from(this.assets.values())
95 .filter(asset => asset.status === "operational");
96 }
97
98 getStatusChangeHistory(id?: string): typeof this.statusChangeLog {
99 if (id) {
100 return this.statusChangeLog.filter(log => log.assetId === id);
101 }
102 return [...this.statusChangeLog];
103 }
104
105 generateReport(): string {
106 const totalAssets = this.assets.size;
107 const operational = this.getOperationalAssets().length;
108 const maintenance = Array.from(this.assets.values())
109 .filter(asset => asset.status === "maintenance").length;
110 const offline = Array.from(this.assets.values())
111 .filter(asset => asset.status === "offline").length;
112
113 return `
114ASSET STATUS REPORT: ${this.assetType.toUpperCase()}
115======================================
116Total count: ${totalAssets}
117Operacyjne: ${operational} (${Math.round(operational / totalAssets * 100)}%)
118W konserwacji: ${maintenance} (${Math.round(maintenance / totalAssets * 100)}%)
119Offline: ${offline} (${Math.round(offline / totalAssets * 100)}%)
120======================================
121Wygenerowano: ${new Date().toLocaleString()}
122 `;
123 }
124}
125
126// Using the asset management system with different types
127const vehicleManager = new JurassicParkAssetManager<Vehicle>("Pojazdy");
128const facilityManager = new JurassicParkAssetManager<Facility>("Obiekty");
129const powerManager = new JurassicParkAssetManager<PowerSystem>("Zasilanie");
130
131// Registering sample assets
132vehicleManager.registerAsset({
133 id: "VEH-001",
134 name: "Explorer 04",
135 status: "operational",
136 lastUpdated: new Date(),
137 type: "jeep",
138 passengerCapacity: 5,
139 currentFuel: 75
140});
141
142vehicleManager.registerAsset({
143 id: "VEH-002",
144 name: "Explorer 05",
145 status: "maintenance",
146 lastUpdated: new Date(),
147 type: "jeep",
148 passengerCapacity: 5,
149 currentFuel: 30
150});
151
152facilityManager.registerAsset({
153 id: "FAC-001",
154 name: "Visitor Center",
155 status: "operational",
156 lastUpdated: new Date(),
157 type: "visitor",
158 location: { x: 100, y: 200 },
159 capacity: 200
160});
161
162powerManager.registerAsset({
163 id: "PWR-001",
164 name: "Main Power Grid",
165 status: "operational",
166 lastUpdated: new Date(),
167 type: "main",
168 outputCapacity: 10000,
169 currentLoad: 6500
170});
171
172// Simulating failures and status updates
173vehicleManager.updateStatus("VEH-001", "maintenance");
174powerManager.updateStatus("PWR-001", "offline");
175facilityManager.updateStatus("FAC-001", "maintenance");
176
177// After the fix
178setTimeout(() => {
179 vehicleManager.updateStatus("VEH-001", "operational");
180 powerManager.updateStatus("PWR-001", "operational");
181 facilityManager.updateStatus("FAC-001", "operational");
182
183 // Generating reports
184 console.log(vehicleManager.generateReport());
185 console.log(powerManager.generateReport());
186
187 // Status change history
188 console.log("Main power status change history:");
189 console.log(powerManager.getStatusChangeHistory("PWR-001"));
190}, 3000);Generic classes are especially useful in the following situations:
Generic classes are a powerful tool in TypeScript that allows creating flexible, type-safe code. They provide an excellent balance between reusability and type safety, enabling the creation of components that:
Just like in Jurassic Park, where infrastructure must be flexible to accommodate different dinosaur species, generic classes allow us to build systems that are both flexible and safe - the perfect combination for modern software development.
In the next exercise, we'll look at how we can further refine our generics using generic constraints to ensure that our generic classes and functions only work with types that meet specific requirements.