Kurs JavaScript i TypeScript · Moduł 7: Zaawansowany TypeScript
Ograniczenia generyczne (constraints)
W tej lekcji9
"W Parku Jurajskim nie wszystkie dinozaury mogą być umieszczone w tych samych zagrodach," tłumaczy Dr. Alan Grant, główny paleontolog parku. "Roślinożerne triceratopsy mogą żyć razem, ale nie możemy umieścić ich z drapieżnymi welociraptorami. Potrzebujemy jasnych zasad, które dinozaury mogą być gdzie umieszczone."
W TypeScript ograniczenia generyczne (generic constraints) działają na podobnej zasadzie - pozwalają określić wymagania dla typów, które mogą być użyte z generycznymi funkcjami lub klasami. Dzięki nim możemy tworzyć bardziej precyzyjne i bezpieczne API, zapewniając, że tylko odpowiednie typy danych są akceptowane.
Podstawy ograniczeń generycznych
Ograniczenia generyczne tworzymy przy użyciu słowa kluczowego extends. Pozwala to na ograniczenie zakresu możliwych typów, które mogą być użyte jako argumenty typu.
Przykład: Proste ograniczenie generyczne
Rozważmy funkcję, która ma wyświetlać podstawowe informacje o dinozaurze. Zamiast akceptować dowolny typ, chcemy mieć pewność, że typ przekazany do funkcji ma przynajmniej właściwości name i species:
1// Ograniczenie generyczne - typ T musi mieć przynajmniej właściwości name i species
2function displayDinosaurInfo<T extends { name: string; species: string }>(dino: T): void {
3 console.log(`Nazwa: ${dino.name}, Gatunek: ${dino.species}`);
4}
5
6// Działa, ponieważ obiekt zawiera wymagane właściwości
7displayDinosaurInfo({ name: "Blue", species: "Velociraptor", age: 5 });
8
9// Działa również z bardziej złożonymi obiektami, o ile zawierają wymagane właściwości
10displayDinosaurInfo({
11 name: "Rex",
12 species: "Tyrannosaurus Rex",
13 age: 7,
14 weight: 8000,
15 dangerLevel: "Extreme"
16});
17
18// Błąd kompilacji - brak wymaganej właściwości 'species'
19// displayDinosaurInfo({ name: "Charlie", age: 4 });W tym przykładzie T extends { name: string; species: string } oznacza, że typ generyczny T musi zawierać co najmniej właściwości name i species odpowiedniego typu. Możemy przekazać obiekt z dodatkowymi właściwościami, ale te dwie są wymagane.
Ograniczenia na interfejsach i typach
Często definiujemy interfejsy lub typy, a następnie używamy ich jako ograniczeń dla naszych typów generycznych:
1// Definiujemy interfejs podstawowy
2interface DinosaurBase {
3 id: string;
4 name: string;
5 species: string;
6 age: number;
7}
8
9// Funkcja z ograniczeniem generycznym opartym na interfejsie
10function addToRegistry<T extends DinosaurBase>(dino: T): void {
11 console.log(`Dodano do rejestru: ${dino.name} (ID: ${dino.id})`);
12 // Tutaj moglibyśmy dodać dinozaura do bazy danych, itp.
13}
14
15// Działa, ponieważ obiekt implementuje DinosaurBase
16addToRegistry({
17 id: "V-001",
18 name: "Delta",
19 species: "Velociraptor",
20 age: 6,
21 trainingLevel: 3 // Dodatkowa właściwość jest ok
22});
23
24// Błąd kompilacji - brak wymaganej właściwości 'age'
25// addToRegistry({
26// id: "T-001",
27// name: "Rexy",
28// species: "Tyrannosaurus"
29// });Ograniczenia dla klas generycznych
Ograniczenia generyczne są szczególnie przydatne w klasach generycznych, gdzie chcemy zapewnić, że typy użyte z klasą będą spełniać określone wymagania:
1// Interfejs dla zwierząt, które mogą być monitorowane
2interface Trackable {
3 id: string;
4 lastKnownLocation: { x: number; y: number };
5 attachTracker(trackerId: string): void;
6}
7
8// Klasa generyczna z ograniczeniem
9class DinosaurTracker<T extends Trackable> {
10 private trackedEntities: T[] = [];
11
12 addEntity(entity: T): void {
13 // Możemy bezpiecznie używać metod z interfejsu Trackable
14 entity.attachTracker(`TRK-${Date.now()}`);
15 this.trackedEntities.push(entity);
16 console.log(`Rozpoczęto śledzenie: ID ${entity.id} na pozycji (${entity.lastKnownLocation.x}, ${entity.lastKnownLocation.y})`);
17 }
18
19 updateLocation(id: string, x: number, y: number): void {
20 const entity = this.trackedEntities.find(e => e.id === id);
21 if (entity) {
22 entity.lastKnownLocation = { x, y };
23 console.log(`Zaktualizowano pozycję ${id}: (${x}, ${y})`);
24 }
25 }
26
27 getEntities(): readonly T[] {
28 return [...this.trackedEntities];
29 }
30}
31
32// Implementacja klasy dinozaura, która spełnia wymagania interfejsu Trackable
33class Velociraptor implements Trackable {
34 id: string;
35 name: string;
36 lastKnownLocation: { x: number; y: number };
37 private tracker: string | null = null;
38
39 constructor(id: string, name: string, initialX: number, initialY: number) {
40 this.id = id;
41 this.name = name;
42 this.lastKnownLocation = { x: initialX, y: initialY };
43 }
44
45 attachTracker(trackerId: string): void {
46 this.tracker = trackerId;
47 console.log(`Przypięto tracker ${trackerId} do velociraptora ${this.name}`);
48 }
49
50 hunt(): void {
51 // Logika polowania
52 console.log(`${this.name} poluje w okolicy (${this.lastKnownLocation.x}, ${this.lastKnownLocation.y})`);
53 }
54}
55
56// Użycie trackera z velociraptorami
57const raptorTracker = new DinosaurTracker<Velociraptor>();
58
59const blue = new Velociraptor("V-001", "Blue", 120, 340);
60const charlie = new Velociraptor("V-002", "Charlie", 125, 330);
61
62raptorTracker.addEntity(blue);
63raptorTracker.addEntity(charlie);
64
65// Możemy użyć metod specyficznych dla Velociraptor, ponieważ znamy konkretny typ
66const raptors = raptorTracker.getEntities();
67raptors.forEach(raptor => raptor.hunt());
68
69// Symulacja ruchu
70raptorTracker.updateLocation("V-001", 140, 360);Ograniczenia z wieloma wymaganiami
Czasami potrzebujemy, aby typ generyczny spełniał wymagania z wielu interfejsów lub typów. Możemy używać znaku & (intersection type) do łączenia ograniczeń:
1// Dwa interfejsy reprezentujące różne aspekty dynamiki parku
2interface Identifiable {
3 id: string;
4 serialNumber?: string;
5}
6
7interface Statusable {
8 status: "active" | "inactive" | "maintenance";
9 lastStatusUpdate: Date;
10 updateStatus(newStatus: "active" | "inactive" | "maintenance"): void;
11}
12
13// Funkcja generyczna, która wymaga, aby typ spełniał oba interfejsy
14function performMaintenanceCheck<T extends Identifiable & Statusable>(entity: T): void {
15 console.log(`Rozpoczęcie przeglądu dla ${entity.id}`);
16
17 if (entity.status !== "maintenance") {
18 entity.updateStatus("maintenance");
19 console.log(`Status zaktualizowany do "maintenance" o ${entity.lastStatusUpdate.toLocaleTimeString()}`);
20 }
21
22 // Symulacja przeglądu
23 setTimeout(() => {
24 entity.updateStatus("active");
25 console.log(`Przegląd zakończony dla ${entity.id}. Status: active`);
26 }, 2000);
27}
28
29// Klasa, która implementuje oba interfejsy
30class SecurityGate implements Identifiable, Statusable {
31 id: string;
32 serialNumber: string;
33 status: "active" | "inactive" | "maintenance";
34 lastStatusUpdate: Date;
35
36 constructor(id: string, serialNumber: string) {
37 this.id = id;
38 this.serialNumber = serialNumber;
39 this.status = "active";
40 this.lastStatusUpdate = new Date();
41 }
42
43 updateStatus(newStatus: "active" | "inactive" | "maintenance"): void {
44 this.status = newStatus;
45 this.lastStatusUpdate = new Date();
46
47 // Dodatkowa logika specyficzna dla bramy
48 if (newStatus === "active") {
49 console.log(`Brama ${this.id} jest teraz aktywna i zabezpieczona`);
50 } else if (newStatus === "inactive") {
51 console.log(`UWAGA: Brama ${this.id} jest nieaktywna. Zagrożenie bezpieczeństwa!`);
52 }
53 }
54
55 // Metoda specyficzna dla bramy
56 toggleLock(locked: boolean): void {
57 console.log(`Brama ${this.id} jest teraz ${locked ? "zablokowana" : "odblokowana"}`);
58 }
59}
60
61// Użycie funkcji z obiektem, który spełnia oba ograniczenia
62const mainGate = new SecurityGate("GATE-001", "SN123456");
63performMaintenanceCheck(mainGate);
64
65// Dodatkowa funkcjonalność dostępna dzięki znajomości pełnego typu
66mainGate.toggleLock(true);Ograniczenia z elementami wariantowymi (keyof)
Operator keyof w TypeScript pozwala nam uzyskać sumę kluczy obiektu jako typ. Możemy łączyć to z ograniczeniami generycznymi, aby tworzyć funkcje, które działają na określonych właściwościach obiektów:
1// Funkcja, która pobiera obiekt i nazwę pola, a następnie zwraca wartość tego pola
2function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
3 return obj[key];
4}
5
6// Przykładowy obiekt z danymi dinozaura
7const trex = {
8 name: "Rexy",
9 species: "Tyrannosaurus Rex",
10 age: 7,
11 diet: "carnivore",
12 threatLevel: 9,
13 enclosureId: "E-005"
14};
15
16// Poprawne użycie - 'name' jest kluczem obiektu trex
17const dinoName = getProperty(trex, "name");
18console.log(dinoName); // "Rexy"
19
20// Poprawne użycie - 'age' jest kluczem obiektu trex
21const dinoAge = getProperty(trex, "age");
22console.log(dinoAge); // 7
23
24// Błąd kompilacji - 'habitat' nie jest kluczem obiektu trex
25// const habitat = getProperty(trex, "habitat");To ograniczenie zapewnia, że klucz K jest faktycznie nazwą właściwości obiektu T. TypeScript wie również, jaki typ wartości zostanie zwrócony: T[K] (typ wartości pod kluczem K w obiekcie typu T).
Praktyczny przykład: System raportowania dla Parku Jurajskiego
Zobaczmy bardziej złożony przykład, który wykorzystuje różne aspekty ograniczeń generycznych w kontekście systemu raportowania dla Parku Jurajskiego:
1// --- Definicje bazowych typów ---
2
3// Bazowy interfejs dla wszystkich zasobów parku
4interface ParkResource {
5 id: string;
6 name: string;
7 lastUpdated: Date;
8}
9
10// Interfejs dla generowania raportów
11interface Reportable {
12 generateReport(): string;
13}
14
15// Interfejs dla zasobów, którymi można zarządzać
16interface Manageable {
17 assignTo(employeeId: string): void;
18 getAssignee(): string | null;
19}
20
21// --- Specyficzne typy zasobów ---
22
23interface Dinosaur extends ParkResource {
24 species: string;
25 diet: "carnivore" | "herbivore" | "omnivore";
26 healthStatus: "healthy" | "sick" | "critical";
27 enclosureId: string;
28}
29
30interface Employee extends ParkResource {
31 role: string;
32 department: "research" | "security" | "operations" | "visitor";
33 clearanceLevel: 1 | 2 | 3 | 4 | 5;
34}
35
36interface Facility extends ParkResource {
37 type: "enclosure" | "lab" | "visitor_center" | "power_plant";
38 status: "operational" | "maintenance" | "construction";
39 location: { x: number; y: number };
40}
41
42// --- Implementacja systemu raportowania ---
43
44// Generyczna klasa bazowa dla wszystkich repozytoriów
45class ResourceRepository<T extends ParkResource> {
46 protected resources: Map<string, T> = new Map();
47
48 add(resource: T): void {
49 this.resources.set(resource.id, {
50 ...resource,
51 lastUpdated: new Date()
52 });
53 }
54
55 get(id: string): T | undefined {
56 return this.resources.get(id);
57 }
58
59 getAll(): T[] {
60 return Array.from(this.resources.values());
61 }
62
63 update(id: string, updates: Partial<T>): boolean {
64 const resource = this.resources.get(id);
65 if (!resource) return false;
66
67 this.resources.set(id, {
68 ...resource,
69 ...updates,
70 lastUpdated: new Date()
71 });
72
73 return true;
74 }
75
76 delete(id: string): boolean {
77 return this.resources.delete(id);
78 }
79
80 count(): number {
81 return this.resources.size;
82 }
83}
84
85// Klasa dla zarządzalnych zasobów - dodaje funkcjonalność przypisywania
86class ManageableResourceRepository<T extends ParkResource & Partial<Manageable>>
87 extends ResourceRepository<T> {
88
89 private assignments: Map<string, string> = new Map(); // resourceId -> employeeId
90
91 assignResource(resourceId: string, employeeId: string): boolean {
92 const resource = this.resources.get(resourceId);
93 if (!resource) return false;
94
95 this.assignments.set(resourceId, employeeId);
96
97 // Jeśli zasób implementuje Manageable, wywołujemy jego metodę assignTo
98 if (resource.assignTo) {
99 resource.assignTo(employeeId);
100 }
101
102 return true;
103 }
104
105 getAssignee(resourceId: string): string | null {
106 // Najpierw sprawdzamy, czy zasób implementuje własną metodę getAssignee
107 const resource = this.resources.get(resourceId);
108 if (resource?.getAssignee) {
109 return resource.getAssignee();
110 }
111
112 // W przeciwnym razie korzystamy z naszej wewnętrznej mapy
113 return this.assignments.get(resourceId) || null;
114 }
115
116 getAssignedResources(employeeId: string): T[] {
117 const assigned: T[] = [];
118
119 this.assignments.forEach((assignedEmployeeId, resourceId) => {
120 if (assignedEmployeeId === employeeId) {
121 const resource = this.resources.get(resourceId);
122 if (resource) assigned.push(resource);
123 }
124 });
125
126 return assigned;
127 }
128}
129
130// Klasa dla zasobów, które mogą generować raporty
131class ReportableResourceRepository<T extends ParkResource & Reportable>
132 extends ResourceRepository<T> {
133
134 generateIndividualReport(id: string): string | null {
135 const resource = this.resources.get(id);
136 if (!resource) return null;
137
138 return resource.generateReport();
139 }
140
141 generateSummaryReport(): string {
142 const allResources = this.getAll();
143
144 let report = `RAPORT ZBIORCZY
145=======================
146Łączna liczba zasobów: ${allResources.length}
147Ostatnia aktualizacja: ${new Date().toLocaleString()}
148=======================\n\n`;
149
150 allResources.forEach(resource => {
151 report += `${resource.name} (ID: ${resource.id}):\n`;
152 report += `${resource.generateReport()}\n\n`;
153 });
154
155 return report;
156 }
157}
158
159// --- Implementacje specyficznych repozytoriów ---
160
161// Repozytorium dinozaurów
162class DinosaurRepository extends ManageableResourceRepository<Dinosaur> {
163 // Metody specyficzne dla dinozaurów
164 findBySpecies(species: string): Dinosaur[] {
165 return this.getAll().filter(dino => dino.species === species);
166 }
167
168 findByDiet(diet: Dinosaur["diet"]): Dinosaur[] {
169 return this.getAll().filter(dino => dino.diet === diet);
170 }
171
172 findByEnclosure(enclosureId: string): Dinosaur[] {
173 return this.getAll().filter(dino => dino.enclosureId === enclosureId);
174 }
175
176 // Metoda generująca specjalny raport dla dinozaurów
177 generateHealthStatusReport(): string {
178 const allDinos = this.getAll();
179 const healthy = allDinos.filter(d => d.healthStatus === "healthy").length;
180 const sick = allDinos.filter(d => d.healthStatus === "sick").length;
181 const critical = allDinos.filter(d => d.healthStatus === "critical").length;
182
183 return `
184RAPORT ZDROWIA DINOZAURÓW
185=========================
186Łącznie: ${allDinos.length}
187Zdrowe: ${healthy} (${Math.round(healthy / allDinos.length * 100)}%)
188Chore: ${sick} (${Math.round(sick / allDinos.length * 100)}%)
189Stan krytyczny: ${critical} (${Math.round(critical / allDinos.length * 100)}%)
190=========================
191`;
192 }
193}
194
195// Repozytorium pracowników
196class EmployeeRepository extends ManageableResourceRepository<Employee> {
197 // Metody specyficzne dla pracowników
198 findByDepartment(department: Employee["department"]): Employee[] {
199 return this.getAll().filter(emp => emp.department === department);
200 }
201
202 findByClearanceLevel(level: Employee["clearanceLevel"]): Employee[] {
203 return this.getAll().filter(emp => emp.clearanceLevel === level);
204 }
205}
206
207// --- Przykładowe klasy zasobów z implementacją raportowania ---
208
209// Klasa dinozaura implementująca Reportable
210class ReportableDinosaur implements Dinosaur, Reportable {
211 id: string;
212 name: string;
213 species: string;
214 diet: "carnivore" | "herbivore" | "omnivore";
215 healthStatus: "healthy" | "sick" | "critical";
216 enclosureId: string;
217 lastUpdated: Date;
218
219 // Dodatkowe właściwości
220 private weight: number;
221 private height: number;
222 private behaviors: string[] = [];
223
224 constructor(
225 id: string,
226 name: string,
227 species: string,
228 diet: "carnivore" | "herbivore" | "omnivore",
229 healthStatus: "healthy" | "sick" | "critical",
230 enclosureId: string,
231 weight: number,
232 height: number
233 ) {
234 this.id = id;
235 this.name = name;
236 this.species = species;
237 this.diet = diet;
238 this.healthStatus = healthStatus;
239 this.enclosureId = enclosureId;
240 this.weight = weight;
241 this.height = height;
242 this.lastUpdated = new Date();
243 }
244
245 addBehaviorObservation(behavior: string): void {
246 this.behaviors.push(`${new Date().toISOString()}: ${behavior}`);
247 this.lastUpdated = new Date();
248 }
249
250 generateReport(): string {
251 return `
252--- RAPORT DINOZAURA ---
253ID: ${this.id}
254Nazwa: ${this.name}
255Gatunek: ${this.species}
256Dieta: ${this.diet}
257Stan zdrowia: ${this.healthStatus}
258Zagroda: ${this.enclosureId}
259Waga: ${this.weight} kg
260Wysokość: ${this.height} m
261Ostatnie zachowania: ${this.behaviors.length > 0 ? '\n - ' + this.behaviors.slice(-3).join('\n - ') : 'Brak zapisanych obserwacji'}
262-----------------------
263`;
264 }
265}
266
267// --- Przykład użycia ---
268
269// Tworzenie repozytorium dla raportujących dinozaurów
270const reportableDinoRepo = new ReportableResourceRepository<ReportableDinosaur>();
271
272// Dodawanie dinozaurów do repozytorium
273const rexReport = new ReportableDinosaur(
274 "D-001",
275 "Rexy",
276 "Tyrannosaurus Rex",
277 "carnivore",
278 "healthy",
279 "E-005",
280 8000,
281 5.2
282);
283
284const triceratopsReport = new ReportableDinosaur(
285 "D-002",
286 "Tricy",
287 "Triceratops",
288 "herbivore",
289 "sick",
290 "E-010",
291 6000,
292 3.0
293);
294
295// Dodawanie obserwacji zachowań
296rexReport.addBehaviorObservation("Zjadła całą krowę w ciągu 15 minut");
297rexReport.addBehaviorObservation("Terytorialne ryknięcie w kierunku zagrody Spinozaura");
298
299triceratopsReport.addBehaviorObservation("Słabszy apetyt niż zwykle");
300triceratopsReport.addBehaviorObservation("Oznaki gorączki i senności");
301
302// Dodanie do repozytorium
303reportableDinoRepo.add(rexReport);
304reportableDinoRepo.add(triceratopsReport);
305
306// Generowanie raportów
307console.log(reportableDinoRepo.generateIndividualReport("D-001"));
308console.log(reportableDinoRepo.generateSummaryReport());Ograniczenia ograniczeń generycznych
Mimo że ograniczenia generyczne są potężnym narzędziem, mają pewne ograniczenia:
Niepełna informacja o typie w czasie wykonania - TypeScript używa ograniczeń generycznych głównie w czasie kompilacji. W czasie wykonania informacje o typach są usuwane (type erasure).
Ograniczenia nominalne vs strukturalne - TypeScript używa typowania strukturalnego, co oznacza, że obiekty o tej samej strukturze są uważane za kompatybilne, nawet jeśli mają różne nazwy typów. Niektórzy programiści przyzwyczajeni do typowania nominalnego (jak w Javie czy C#) mogą uznać to za nieoczekiwane.
Brak ograniczeń negatywnych - Nie można określić, że typ generyczny NIE może mieć pewnych właściwości lub NIE może implementować określonego interfejsu.
Pomimo tych ograniczeń, ograniczenia generyczne są niezwykle przydatne w codziennej pracy z TypeScript.
Kiedy stosować ograniczenia generyczne?
Ograniczenia generyczne warto stosować, gdy:
Potrzebujesz pisać funkcje lub klasy, które działają na różnych typach, ale wymagają, aby te typy miały pewne wspólne cechy.
Chcesz być pewien, że określone metody lub właściwości będą dostępne w typach używanych z twoimi generycznymi komponentami.
Potrzebujesz stworzyć precyzyjne API, które zapewnia bezpieczeństwo typów przy zachowaniu elastyczności.
Implementujesz wzorce projektowe, takie jak repozytoria, fabryki czy serwisy, które powinny działać z różnymi, ale powiązanymi typami danych.
Podsumowanie
Ograniczenia generyczne są ważnym narzędziem w TypeScript, które pozwala na tworzenie bezpiecznych pod względem typów, ale jednocześnie elastycznych komponentów. Pozwalają określić, jakie wymagania muszą spełniać typy używane z generycznymi funkcjami i klasami, co prowadzi do bardziej precyzyjnych i niezawodnych interfejsów API.
Podobnie jak w Parku Jurajskim, gdzie musimy mieć jasne zasady, które dinozaury mogą być umieszczone w których zagrodach, ograniczenia generyczne pozwalają nam jasno określić, jakie typy mogą być używane z naszymi generycznymi komponentami. To zapewnia bezpieczeństwo i przewidywalność naszego kodu, jednocześnie zachowując elastyczność, która sprawia, że generyki są tak potężnym narzędziem.
Kod do tej lekcji: index.ts
1// Ograniczenia generyczne (constraints) - Park Jurajski
2console.log("=== OGRANICZENIA GENERYCZNE ===\n");
3
4// ===========================================
5// 1. extends - ograniczenie typu
6// ===========================================
7
8interface HasName {
9 name: string;
10}
11
12interface HasWeight {
13 weight: number;
14}
15
16// T musi miec pole 'name'
17function printName<T extends HasName>(entity: T): void {
18 console.log(`Nazwa: ${entity.name}`);
19}
20
21// T musi miec zarowno 'name' jak i 'weight'
22function getHeaviest<T extends HasName & HasWeight>(items: T[]): T | undefined {
23 if (items.length === 0) return undefined;
24 return items.reduce((max, item) => item.weight > max.weight ? item : max);
25}
26
27// ===========================================
28// 2. keyof - bezpieczny dostep do kluczy
29// ===========================================
30
31interface DinosaurProfile {
32 name: string;
33 species: string;
34 weight: number;
35 height: number;
36 dangerLevel: number;
37}
38
39// K musi byc kluczem T
40function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
41 return obj[key];
42}
43
44// Pobranie wielu wlasciwosci naraz
45function pick<T, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> {
46 const result = {} as Pick<T, K>;
47 keys.forEach(key => {
48 result[key] = obj[key];
49 });
50 return result;
51}
52
53// ===========================================
54// 3. Ograniczenia z domyslnymi typami
55// ===========================================
56
57interface Sortable {
58 compareTo?(other: this): number;
59}
60
61function sortEntities<T extends HasName>(
62 items: T[],
63 compareFn: (a: T, b: T) => number = (a, b) => a.name.localeCompare(b.name)
64): T[] {
65 return [...items].sort(compareFn);
66}
67
68// ===========================================
69// 4. Ograniczenie konstruktora
70// ===========================================
71
72interface Constructor<T> {
73 new (...args: any[]): T;
74}
75
76function createInstances<T extends HasName>(
77 Ctor: Constructor<T>,
78 count: number,
79 ...args: any[]
80): T[] {
81 const instances: T[] = [];
82 for (let i = 0; i < count; i++) {
83 instances.push(new Ctor(...args));
84 }
85 return instances;
86}
87
88// ===========================================
89// Test
90// ===========================================
91
92const dinosaurs: DinosaurProfile[] = [
93 { name: "Rexy", species: "T-Rex", weight: 8000, height: 5.5, dangerLevel: 10 },
94 { name: "Blue", species: "Velociraptor", weight: 15, height: 1.8, dangerLevel: 8 },
95 { name: "Patty", species: "Brachiosaurus", weight: 50000, height: 16, dangerLevel: 1 },
96 { name: "Cera", species: "Triceratops", weight: 6000, height: 3, dangerLevel: 3 },
97];
98
99// extends
100console.log("--- extends constraint ---");
101printName({ name: "Rexy" }); // OK - ma pole 'name'
102// printName({ age: 5 }); // Error! - brak 'name'
103
104const heaviest = getHeaviest(dinosaurs);
105console.log(`Najciezszy: ${heaviest?.name} (${heaviest?.weight}kg)`);
106
107// keyof
108console.log("\n--- keyof constraint ---");
109const rex = dinosaurs[0];
110console.log("Nazwa:", getProperty(rex, "name"));
111console.log("Waga:", getProperty(rex, "weight"));
112// getProperty(rex, "color"); // Error! 'color' nie jest kluczem
113
114const summary = pick(rex, ["name", "species", "dangerLevel"]);
115console.log("Wybrane pola:", JSON.stringify(summary));
116
117// Sortowanie z ograniczeniem
118console.log("\n--- sortEntities ---");
119const sorted = sortEntities(dinosaurs);
120console.log("Alfabetycznie:", sorted.map(d => d.name).join(", "));
121
122const byDanger = sortEntities(dinosaurs, (a, b) => b.dangerLevel - a.dangerLevel);
123console.log("Po zagrozeniu:", byDanger.map(d => `${d.name}(${d.dangerLevel})`).join(", "));
124
125// Wielokrotne ograniczenia
126console.log("\n--- Multiple constraints ---");
127function describeEntity<T extends HasName & HasWeight>(entity: T): string {
128 return `${entity.name} wazy ${entity.weight}kg`;
129}
130dinosaurs.forEach(d => console.log(describeEntity(d)));
131
132console.log("\nConstraints = bezpieczne generyki z gwarancja 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. Co oznacza zapis <T extends { length: number }> w TypeScript?
2. Czym jest constraint w generykach TypeScript?
Zadania praktyczne w grze
- Edytor kodu
Użyj constraints do bezpiecznego typowo dostępu do danych
- Układanie w poziomie
Ułóż elementy użycia keyof:
- Układanie w pionie
Uporządkuj constraints od najbardziej ogólnego do najbardziej szczegółowego: