Kurs JavaScript i TypeScript · Moduł 7: Zaawansowany TypeScript

Ograniczenia generyczne (constraints)

12 min czytania
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:

  1. 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).

  2. 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.

  3. 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:

  1. Potrzebujesz pisać funkcje lub klasy, które działają na różnych typach, ale wymagają, aby te typy miały pewne wspólne cechy.

  2. Chcesz być pewien, że określone metody lub właściwości będą dostępne w typach używanych z twoimi generycznymi komponentami.

  3. Potrzebujesz stworzyć precyzyjne API, które zapewnia bezpieczeństwo typów przy zachowaniu elastyczności.

  4. 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. 1. Co oznacza zapis <T extends { length: number }> w TypeScript?

  2. 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:

Przydatne artykuły