Kurs JavaScript i TypeScript · Moduł 7: Zaawansowany TypeScript
Klasy generyczne
W tej lekcji9
"Każda zagroda w Parku Jurajskim musi być dostosowana do specyficznego gatunku dinozaura," wyjaśnia Dr. Ellie Sattler, kierowniczka działu badań botanicznych. "Ale mimo różnic, wszystkie zagrody działają według tych samych zasad - kontrola wejść, monitorowanie warunków środowiskowych, zautomatyzowane systemy karmienia. To, co różni zagrodę dla welociraptora od zagrody dla triceratopsa, to nie jej struktura, ale parametry."
Tak samo działają klasy generyczne w TypeScript. Pozwalają na tworzenie wielokrotnie używalnych komponentów, które mogą pracować z różnymi typami danych, przy jednoczesnym zachowaniu pełnej kontroli typów. Jeśli w funkcjach generycznych zobaczyliśmy podstawową ideę generyczności, to klasy generyczne pokazują pełny potencjał tego mechanizmu.
Czym są klasy generyczne?
Klasy generyczne to szablony klas, które mogą działać z różnymi typami danych. Podobnie jak funkcje generyczne, używają one parametrów typu (np. <T>), ale pozwalają na wykorzystanie tych parametrów w całej klasie - w polach, metodach i konstruktorach.
Oto prosta klasa generyczna reprezentująca zagrodę dla dinozaurów:
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("Dodano nowego mieszkańca zagrody!");
11 }
12
13 getInhabitants(): T[] {
14 return [...this.inhabitants]; // Zwracamy kopię, aby uniknąć bezpośredniej modyfikacji
15 }
16
17 countInhabitants(): number {
18 return this.inhabitants.length;
19 }
20}Powyższa klasa może być użyta do stworzenia zagrody dla dowolnego typu dinozaurów:
1// Definicje typów dinozaurów
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// Tworzenie wyspecjalizowanych zagród
15const raptorEnclosure = new DinosaurEnclosure<Velociraptor>();
16const triceratopsEnclosure = new DinosaurEnclosure<Triceratops>();
17
18// Dodawanie mieszkańców do zagród
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// Sprawdzanie mieszkańców
32console.log(`Liczba welociraptorów: ${raptorEnclosure.countInhabitants()}`);
33console.log(`Liczba triceratopsów: ${triceratopsEnclosure.countInhabitants()}`);
34
35// TypeScript zapewnia bezpieczeństwo typów
36// To nie skompiluje się:
37// raptorEnclosure.addInhabitant({
38// name: "Invalid",
39// hornLength: 50,
40// weight: 7000
41// });Klasy z wieloma parametrami typów
Klasy generyczne mogą używać wielu parametrów typów, co pozwala na jeszcze większą elastyczność:
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(`Dodano próbkę #${id}`);
8 }
9
10 recordResult(specimenId: string, result: TResult): void {
11 this.results.set(specimenId, result);
12 console.log(`Zapisano wynik dla próbki #${specimenId}`);
13 }
14
15 getResult(specimenId: string): TResult | undefined {
16 return this.results.get(specimenId);
17 }
18}
19
20// Przykład użycia z konkretnymi typami
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"}`);Rozszerzanie klas generycznych
Klasy generyczne mogą być również rozszerzane, co pozwala na tworzenie bardziej specjalizowanych implementacji:
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(); // Wywołanie konstruktora klasy bazowej
7 this.securityLevel = securityLevel;
8 }
9
10 performSecurityCheck(): void {
11 this.lastCheckTime = new Date();
12 console.log(`Kontrola bezpieczeństwa wykonana o ${this.lastCheckTime.toLocaleTimeString()}`);
13 console.log(`Poziom zabezpieczeń: ${this.securityLevel}`);
14 console.log(`Liczba mieszkańców: ${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(`Zaktualizowano poziom zabezpieczeń do: ${this.securityLevel}`);
28 }
29}
30
31// Użycie rozszerzonej klasy
32const monitoredRaptorEnclosure = new MonitoredEnclosure<Velociraptor>("high");
33
34monitoredRaptorEnclosure.addInhabitant({
35 name: "Delta",
36 speed: 65,
37 clawLength: 14
38});
39
40monitoredRaptorEnclosure.performSecurityCheck();Parametry typów domyślnych
Podobnie jak funkcje mogą mieć parametry domyślne, klasy generyczne mogą mieć domyślne typy dla swoich parametrów typu:
1// Domyślny interfejs dinozaura
2interface DefaultDinosaur {
3 name: string;
4 species: string;
5 age: number;
6}
7
8// Klasa z domyślnym parametrem typu
9class DinosaurTracker<T = DefaultDinosaur> {
10 private trackedDinos: T[] = [];
11
12 trackDinosaur(dino: T, location: string): void {
13 this.trackedDinos.push(dino);
14 console.log(`Rozpoczęto śledzenie dinozaura w lokalizacji: ${location}`);
15 }
16
17 getTrackedCount(): number {
18 return this.trackedDinos.length;
19 }
20}
21
22// Użycie z domyślnym typem
23const defaultTracker = new DinosaurTracker();
24defaultTracker.trackDinosaur({
25 name: "Rexy",
26 species: "Tyrannosaurus Rex",
27 age: 7
28}, "Sektor B");
29
30// Użycie z niestandardowym typem
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");Statyczne właściwości w klasach generycznych
Ważne jest, aby zrozumieć, że parametry typu w klasach generycznych odnoszą się tylko do właściwości instancji i metod. Statyczne właściwości i metody są współdzielone między wszystkimi instancjami klasy, niezależnie od ich parametrów typu:
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(`Zarejestrowano nowego dinozaura. Łącznie: ${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(`Łączna liczba zarejestrowanych dinozaurów: ${DinosaurRegistry.getTotalRegisteredCount()}`); // 2W tym przykładzie, statyczna zmienna totalRegisteredDinos jest wspólna dla wszystkich instancji DinosaurRegistry, niezależnie od ich parametrów typu.
Klasy generyczne a interfejsy generyczne
Klasy generyczne często współpracują z interfejsami generycznymi, tworząc kompleksowy system typów:
1// Interfejs generyczny
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// Implementacja klasy generycznej zgodnie z interfejsem generycznym
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// Użycie repozytorium z konkretnym typem
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 dinozaur: ${rex?.name}, Gatunek: ${rex?.species}`);Praktyczny przykład: System zarządzania assetami Parku Jurajskiego
Rozbudujmy teraz naszą wiedzę o klasy generyczne, implementując kompleksowy system zarządzania zasobami (assetami) Parku Jurajskiego:
1// Podstawowy interfejs dla wszystkich assetów parku
2interface ParkAsset {
3 id: string;
4 name: string;
5 status: "operational" | "maintenance" | "offline";
6 lastUpdated: Date;
7}
8
9// Różne typy assetów
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// Generyczny interfejs dla zarządzania assetami
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// Implementacja generycznej klasy zarządzającej assetami
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(`Zainicjalizowano system zarządzania assetami typu: ${assetType}`);
43 }
44
45 registerAsset(asset: T): void {
46 if (this.assets.has(asset.id)) {
47 throw new Error(`Asset o ID ${asset.id} już istnieje`);
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 // Aktualizacja statusu
73 const updatedAsset = {
74 ...asset,
75 status,
76 lastUpdated: new Date()
77 };
78
79 this.assets.set(id, updatedAsset);
80
81 // Logowanie zmiany
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 `
114RAPORT STATUSU ASSETÓW: ${this.assetType.toUpperCase()}
115======================================
116Łączna liczba: ${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// Użycie systemu zarządzania assetami z różnymi typami
127const vehicleManager = new JurassicParkAssetManager<Vehicle>("Pojazdy");
128const facilityManager = new JurassicParkAssetManager<Facility>("Obiekty");
129const powerManager = new JurassicParkAssetManager<PowerSystem>("Zasilanie");
130
131// Rejestracja przykładowych assetów
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// Symulacja awarii i aktualizacji statusów
173vehicleManager.updateStatus("VEH-001", "maintenance");
174powerManager.updateStatus("PWR-001", "offline");
175facilityManager.updateStatus("FAC-001", "maintenance");
176
177// Po naprawie
178setTimeout(() => {
179 vehicleManager.updateStatus("VEH-001", "operational");
180 powerManager.updateStatus("PWR-001", "operational");
181 facilityManager.updateStatus("FAC-001", "operational");
182
183 // Generowanie raportów
184 console.log(vehicleManager.generateReport());
185 console.log(powerManager.generateReport());
186
187 // Historia zmian statusu
188 console.log("Historia zmian statusu głównego zasilania:");
189 console.log(powerManager.getStatusChangeHistory("PWR-001"));
190}, 3000);Kiedy używać klas generycznych?
Klasy generyczne są szczególnie przydatne w następujących sytuacjach:
- Kolekcje i kontenery danych - gdy potrzebujesz przechowywać i zarządzać elementami różnych typów
- Serwisy i repozytoria - gdy implementujesz wzorce projektowe, które powinny działać z różnymi typami danych
- Komponenty wielokrotnego użytku - gdy chcesz stworzyć elastyczne komponenty, które mogą być używane w różnych kontekstach
- Abstrakcje na różnych poziomach - gdy chcesz zdefiniować hierarchie klas, które mogą być specjalizowane dla różnych typów
Podsumowanie
Klasy generyczne są potężnym narzędziem w TypeScript, które pozwala na tworzenie elastycznego, bezpiecznego pod względem typów kodu. Zapewniają one doskonałą równowagę między reużywalnością a bezpieczeństwem typów, umożliwiając tworzenie komponentów, które:
- Mogą działać z różnymi typami danych
- Zachowują pełne informacje o typach
- Zapewniają wsparcie kompilatora i IDE (podpowiedzi, weryfikacja typów)
- Są czytelne i zrozumiałe dla innych programistów
Podobnie jak w Parku Jurajskim, gdzie infrastruktura musi być elastyczna, aby obsłużyć różne gatunki dinozaurów, klasy generyczne pozwalają nam budować systemy, które są zarówno elastyczne, jak i bezpieczne - idealne połączenie dla nowoczesnego rozwoju oprogramowania.
W następnym ćwiczeniu przyjrzymy się, jak możemy dodatkowo udoskonalić nasze generyki, używając ograniczeń generycznych (constraints), aby zapewnić, że nasze generyczne klasy i funkcje działają tylko z typami, które spełniają określone wymagania.
Kod do tej lekcji: index.ts
1// Klasy generyczne - Park Jurajski
2console.log("=== KLASY GENERYCZNE ===\n");
3
4// ===========================================
5// 1. GenericRepository<T> - repozytorium danych
6// ===========================================
7
8interface Identifiable {
9 id: number;
10 name: string;
11}
12
13class GenericRepository<T extends Identifiable> {
14 private items: Map<number, T> = new Map();
15 private label: string;
16
17 constructor(label: string) {
18 this.label = label;
19 }
20
21 add(item: T): void {
22 this.items.set(item.id, item);
23 }
24
25 getById(id: number): T | undefined {
26 return this.items.get(id);
27 }
28
29 getAll(): T[] {
30 return Array.from(this.items.values());
31 }
32
33 remove(id: number): boolean {
34 return this.items.delete(id);
35 }
36
37 count(): number {
38 return this.items.size;
39 }
40
41 find(predicate: (item: T) => boolean): T[] {
42 return this.getAll().filter(predicate);
43 }
44
45 toString(): string {
46 return `[${this.label}] ${this.count()} rekordow`;
47 }
48}
49
50// ===========================================
51// 2. Collection<T> - kolekcja z iteracja
52// ===========================================
53
54class Collection<T> {
55 private elements: T[] = [];
56
57 add(...items: T[]): Collection<T> {
58 this.elements.push(...items);
59 return this; // Fluent API
60 }
61
62 first(): T | undefined {
63 return this.elements[0];
64 }
65
66 last(): T | undefined {
67 return this.elements[this.elements.length - 1];
68 }
69
70 map<U>(fn: (item: T) => U): Collection<U> {
71 const result = new Collection<U>();
72 result.add(...this.elements.map(fn));
73 return result;
74 }
75
76 filter(fn: (item: T) => boolean): Collection<T> {
77 const result = new Collection<T>();
78 result.add(...this.elements.filter(fn));
79 return result;
80 }
81
82 toArray(): T[] {
83 return [...this.elements];
84 }
85
86 get size(): number {
87 return this.elements.length;
88 }
89}
90
91// ===========================================
92// 3. Pair<A, B> - para wartosci
93// ===========================================
94
95class Pair<A, B> {
96 constructor(
97 public readonly first: A,
98 public readonly second: B
99 ) {}
100
101 swap(): Pair<B, A> {
102 return new Pair(this.second, this.first);
103 }
104
105 toString(): string {
106 return `(${this.first}, ${this.second})`;
107 }
108}
109
110// ===========================================
111// Test
112// ===========================================
113
114// --- GenericRepository ---
115interface Dinosaur {
116 id: number;
117 name: string;
118 species: string;
119 weight: number;
120}
121
122interface Enclosure {
123 id: number;
124 name: string;
125 capacity: number;
126}
127
128const dinoRepo = new GenericRepository<Dinosaur>("Dinozaury");
129dinoRepo.add({ id: 1, name: "Rexy", species: "T-Rex", weight: 8000 });
130dinoRepo.add({ id: 2, name: "Blue", species: "Velociraptor", weight: 15 });
131dinoRepo.add({ id: 3, name: "Cera", species: "Triceratops", weight: 6000 });
132
133const encRepo = new GenericRepository<Enclosure>("Zagodzenia");
134encRepo.add({ id: 101, name: "Strefa T-Rex", capacity: 2 });
135encRepo.add({ id: 102, name: "Dolina Raptorow", capacity: 6 });
136
137console.log(dinoRepo.toString());
138console.log(encRepo.toString());
139
140console.log("\nDinozaur #2:", dinoRepo.getById(2)?.name);
141console.log("Ciezkie (>1000kg):", dinoRepo.find(d => d.weight > 1000).map(d => d.name).join(", "));
142
143// --- Collection ---
144console.log("\n--- Collection z fluent API ---");
145const names = new Collection<string>()
146 .add("Rexy", "Blue", "Delta", "Charlie", "Cera")
147 .filter(n => n.length <= 4)
148 .map(n => n.toUpperCase());
149
150console.log("Krotkie nazwy:", names.toArray().join(", "));
151console.log("Pierwsza:", names.first());
152console.log("Ostatnia:", names.last());
153
154// --- Pair ---
155console.log("\n--- Pair ---");
156const dinoEnclosure = new Pair<string, string>("Rexy", "Strefa T-Rex");
157console.log("Para:", dinoEnclosure.toString());
158
159const swapped = dinoEnclosure.swap();
160console.log("Zamieniona:", swapped.toString());
161
162const idWeight = new Pair<number, number>(1, 8000);
163console.log("ID-Waga:", idWeight.toString());
164
165console.log("\nKlasy generyczne zapewniaja bezpieczenstwo typow!");Widzisz błąd w tej lekcji?
Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. Kiedy lepiej użyć generyka <T> zamiast union type (string | number)?
Zadania praktyczne w grze
- Edytor kodu
Stwórz generyczne repozytorium danych parku
- Układanie w pionie
Uporządkuj modyfikatory dostępu od najbardziej restrykcyjnego do najszerszego: