Kurs JavaScript i TypeScript · Moduł 6: Podstawy TypeScript

Funkcje generyczne

13 min czytania
W tej lekcji11

Witaj ponownie w najbardziej zaawansowanym laboratorium genetycznym Parku Jurajskiego! Dr. Henry Wu dziś zaprasza nas do zgłębienia jednego z najpotężniejszych mechanizmów TypeScript - funkcji generycznych. Podobnie jak zaawansowana inżynieria genetyczna pozwala nam tworzyć nowe gatunki dinozaurów z różnym DNA, funkcje generyczne pozwalają na tworzenie elastycznych, wielokrotnego użytku rozwiązań, które działają z różnymi typami danych, zachowując jednocześnie pełne bezpieczeństwo typów.

Czym są funkcje generyczne?

Funkcje generyczne to funkcje, które mogą pracować z różnymi typami danych, zachowując przy tym pełną kontrolę typów. Zamiast definiować konkretny typ parametru i zwracanej wartości, używamy parametrów typu (oznaczonych zwykle jako T, U, K itp.), które są "wypełniane" konkretnymi typami w momencie wywołania funkcji.

Wyobraź sobie, że zamiast tworzyć osobną funkcję dla każdego gatunku dinozaura, możesz stworzyć jedną generyczną funkcję, która będzie działała z każdym gatunkiem!

Podstawowa składnia funkcji generycznych

Przyjrzyjmy się, jak definiuje się i używa funkcji generycznych w TypeScript:

1// Prosta funkcja generyczna - zwraca ten sam element, który został przekazany
2function identycznośćGenetyczna<T>(obiekt: T): T {
3  return obiekt;
4}
5
6// Użycie z różnymi typami
7const łańcuchDNA: string = identycznośćGenetyczna<string>("ACGTACGT");
8const wiekTriceratopsa: number = identycznośćGenetyczna<number>(12);
9const aktywnyStatus: boolean = identycznośćGenetyczna<boolean>(true);
10
11// TypeScript często może wywnioskować typ generyczny na podstawie przekazanego argumentu
12const poziomAgresji = identycznośćGenetyczna(7); // TypeScript wnioskuje T jako number

W powyższym przykładzie:

  1. Definiujemy funkcję generyczną identycznośćGenetyczna z parametrem typu T
  2. Funkcja przyjmuje argument typu T i zwraca wartość tego samego typu
  3. Używamy funkcji z różnymi typami: string, number, boolean
  4. W ostatnim przykładzie TypeScript automatycznie wnioskuje typ generyczny na podstawie przekazanego argumentu

Funkcje generyczne z wieloma parametrami typu

Funkcje generyczne mogą mieć wiele parametrów typu, co pozwala na bardziej złożone operacje:

1// Funkcja generyczna z dwoma parametrami typu
2function parujDaneDinozaurów<T, U>(pierwsze: T, drugie: U): [T, U] {
3  return [pierwsze, drugie];
4}
5
6// Przykładowe użycie
7interface DanePodstawoweDinozaura {
8  nazwa: string;
9  gatunek: string;
10  wiek: number;
11}
12
13interface DaneZdrowotne {
14  temperaturaCiała: number;
15  tętno: number;
16  poziomEnergii: number;
17}
18
19const rexPodstawowe: DanePodstawoweDinozaura = {
20  nazwa: "Rex",
21  gatunek: "Tyrannosaurus",
22  wiek: 7
23};
24
25const rexZdrowie: DaneZdrowotne = {
26  temperaturaCiała: 38.2,
27  tętno: 76,
28  poziomEnergii: 8.5
29};
30
31// Łączenie różnych typów danych
32const pełneDaneRexa = parujDaneDinozaurów<DanePodstawoweDinozaura, DaneZdrowotne>(
33  rexPodstawowe,
34  rexZdrowie
35);
36
37console.log(`${pełneDaneRexa[0].nazwa} ma temperaturę ciała ${pełneDaneRexa[1].temperaturaCiała}°C`);

Ograniczenia generyczne (Generic Constraints)

Czasami chcemy, aby nasze funkcje generyczne działały tylko z określonymi typami danych, które spełniają pewne wymagania. Możemy to osiągnąć za pomocą ograniczeń generycznych poprzez słowo kluczowe extends:

1// Definiujemy interfejs opisujący minimalne wymagania
2interface PodstawoweInformacjeDinozaura {
3  id: string;
4  nazwa: string;
5  gatunek: string;
6}
7
8// Funkcja generyczna z ograniczeniem - T musi zawierać co najmniej właściwości z PodstawoweInformacjeDinozaura
9function wyświetlInformacjeDinozaura<T extends PodstawoweInformacjeDinozaura>(dino: T): void {
10  console.log(`ID: ${dino.id}`);
11  console.log(`Nazwa: ${dino.nazwa}`);
12  console.log(`Gatunek: ${dino.gatunek}`);
13
14  // Możemy również uzyskać dostęp do dodatkowych właściwości, które mogą być obecne w T
15  console.log("Dodatkowe informacje:");
16  const podstawoweKlucze = Object.keys({} as PodstawoweInformacjeDinozaura);
17  Object.entries(dino).forEach(([klucz, wartość]) => {
18    if (!podstawoweKlucze.includes(klucz)) {
19      console.log(`- ${klucz}: ${wartość}`);
20    }
21  });
22}
23
24// Przykład użycia z różnymi typami spełniającymi ograniczenie
25interface TRex extends PodstawoweInformacjeDinozaura {
26  siłaUgryzienia: number;
27  prędkośćBiegu: number;
28}
29
30const rex: TRex = {
31  id: "TREX-001",
32  nazwa: "Rexy",
33  gatunek: "Tyrannosaurus Rex",
34  siłaUgryzienia: 12800,
35  prędkośćBiegu: 32
36};
37
38interface Velociraptor extends PodstawoweInformacjeDinozaura {
39  inteligencja: number;
40  polowanieWGrupie: boolean;
41}
42
43const raptor: Velociraptor = {
44  id: "VELOC-001",
45  nazwa: "Blue",
46  gatunek: "Velociraptor",
47  inteligencja: 9,
48  polowanieWGrupie: true
49};
50
51// Obie funkcje zadziałają, ponieważ oba typy rozszerzają PodstawoweInformacjeDinozaura
52wyświetlInformacjeDinozaura(rex);
53wyświetlInformacjeDinozaura(raptor);
54
55// To nie zadziała, ponieważ brakuje wymaganych właściwości
56// wyświetlInformacjeDinozaura({ waga: 5000, wiek: 12 });

W powyższym przykładzie:

  1. Definiujemy interfejs PodstawoweInformacjeDinozaura z minimalnymi wymaganiami
  2. Tworzymy funkcję generyczną, która działa tylko z typami rozszerzającymi ten interfejs
  3. Funkcja może bezpiecznie korzystać z właściwości wymaganych przez interfejs
  4. Możemy użyć funkcji z różnymi typami dinozaurów, o ile spełniają podstawowe wymagania

Ograniczenia z kluczami (keyof constraint)

Słowo kluczowe keyof pozwala na ograniczenie parametru generycznego do kluczy innego typu. Jest to niezwykle przydatne przy tworzeniu funkcji, które operują na kluczach obiektów:

1// Funkcja pobierająca wartość właściwości na podstawie jej klucza
2function pobierzWłaściwośćDinozaura<T, K extends keyof T>(obiekt: T, klucz: K): T[K] {
3  return obiekt[klucz];
4}
5
6// Przykładowe dane dinozaura
7interface DaneDinozaura {
8  id: string;
9  nazwa: string;
10  gatunek: string;
11  wiek: number;
12  waga: number;
13  drapieżnik: boolean;
14  cechy: string[];
15}
16
17const triceratops: DaneDinozaura = {
18  id: "TRIC-001",
19  nazwa: "Tricky",
20  gatunek: "Triceratops",
21  wiek: 15,
22  waga: 9000,
23  drapieżnik: false,
24  cechy: ["rogi", "tarcza na głowie", "silne nogi"]
25};
26
27// Użycie funkcji z różnymi kluczami
28const nazwaTriceratopsa = pobierzWłaściwośćDinozaura(triceratops, "nazwa");
29console.log(`Nazwa: ${nazwaTriceratopsa}`); // TypeScript wie, że to string
30
31const wiekTriceratopsa = pobierzWłaściwośćDinozaura(triceratops, "wiek");
32console.log(`Wiek: ${wiekTriceratopsa} lat`); // TypeScript wie, że to number
33
34const cechy = pobierzWłaściwośćDinozaura(triceratops, "cechy");
35console.log(`Cechy: ${cechy.join(", ")}`); // TypeScript wie, że to string[]
36
37// To nie zadziała, ponieważ "nieistniejącaWłaściwość" nie jest kluczem DaneDinozaura
38// pobierzWłaściwośćDinozaura(triceratops, "nieistniejącaWłaściwość");

W tym przykładzie:

  1. Tworzymy funkcję pobierzWłaściwośćDinozaura, która przyjmuje obiekt typu T i klucz K, ograniczony do kluczy T
  2. Funkcja zwraca wartość właściwości o danym kluczu, a TypeScript wie dokładnie, jakiego typu będzie zwrócona wartość
  3. Użycie funkcji z nieistniejącym kluczem zostanie wychwycone przez TypeScript na etapie kompilacji

Domyślne parametry typu

Podobnie jak funkcje mogą mieć wartości domyślne dla swoich parametrów, funkcje generyczne mogą mieć domyślne typy dla parametrów typu:

1// Funkcja generyczna z domyślnym parametrem typu
2function utworzRaportDinozaura<T = DaneDinozaura>(dane: T, etykieta: string = "Raport standardowy"): { dane: T, etykieta: string, timestamp: Date } {
3  return {
4    dane,
5    etykieta,
6    timestamp: new Date()
7  };
8}
9
10// Użycie z domyślnym typem
11const raportStandardowy = utworzRaportDinozaura(triceratops);
12console.log(`Raport: ${raportStandardowy.etykieta}`);
13console.log(`Dane dla: ${raportStandardowy.dane.nazwa}`);
14
15// Użycie z innym typem
16interface DaneKlimatyczne {
17  temperatura: number;
18  wilgotność: number;
19  ciśnienie: number;
20  lokalizacja: string;
21}
22
23const daneKlimatyczneWybiegu: DaneKlimatyczne = {
24  temperatura: 28.5,
25  wilgotność: 72,
26  ciśnienie: 1013,
27  lokalizacja: "Wybieg Triceratopsów"
28};
29
30const raportKlimatyczny = utworzRaportDinozaura<DaneKlimatyczne>(
31  daneKlimatyczneWybiegu,
32  "Raport klimatyczny"
33);
34
35console.log(`Raport: ${raportKlimatyczny.etykieta}`);
36console.log(`Temperatura: ${raportKlimatyczny.dane.temperatura}°C`);

Generyczne typy tablicowe i operacje na kolekcjach

Funkcje generyczne są szczególnie przydatne przy pracy z tablicami i kolekcjami danych:

1// Funkcja generyczna do filtrowania tablicy
2function filtrujKolekcję<T>(kolekcja: T[], predykat: (element: T) => boolean): T[] {
3  return kolekcja.filter(predykat);
4}
5
6// Przykładowe dane dinozaurów
7interface KrótkaInformacjaDinozaura {
8  id: string;
9  nazwa: string;
10  gatunek: string;
11  drapieżnik: boolean;
12  waga: number;
13}
14
15const dinozaury: KrótkaInformacjaDinozaura[] = [
16  { id: "TREX-001", nazwa: "Rex", gatunek: "Tyrannosaurus", drapieżnik: true, waga: 8000 },
17  { id: "VEL-001", nazwa: "Blue", gatunek: "Velociraptor", drapieżnik: true, waga: 150 },
18  { id: "TRIC-001", nazwa: "Tricky", gatunek: "Triceratops", drapieżnik: false, waga: 9000 },
19  { id: "STEG-001", nazwa: "Spike", gatunek: "Stegosaurus", drapieżnik: false, waga: 5000 },
20  { id: "BRACH-001", nazwa: "Brachie", gatunek: "Brachiosaurus", drapieżnik: false, waga: 40000 }
21];
22
23// Filtrowanie dinozaurów drapieżnych
24const drapieżniki = filtrujKolekcję<KrótkaInformacjaDinozaura>(
25  dinozaury,
26  dino => dino.drapieżnik
27);
28
29console.log("Drapieżniki w parku:");
30drapieżniki.forEach(dino => console.log(`- ${dino.nazwa} (${dino.gatunek})`));
31
32// Filtrowanie dinozaurów cięższych niż 10000 kg
33const ciężkieDinozaury = filtrujKolekcję<KrótkaInformacjaDinozaura>(
34  dinozaury,
35  dino => dino.waga > 10000
36);
37
38console.log("\nCiężkie dinozaury:");
39ciężkieDinozaury.forEach(dino => console.log(`- ${dino.nazwa}: ${dino.waga} kg`));
40
41// Bardziej zaawansowana funkcja generyczna do mapowania tablicy
42function przekształćKolekcję<T, U>(kolekcja: T[], transformacja: (element: T) => U): U[] {
43  return kolekcja.map(transformacja);
44}
45
46// Tworzenie prostszych obiektów na podstawie dinozaurów
47const etykietyDinozaurów = przekształćKolekcję<KrótkaInformacjaDinozaura, { nazwa: string, typ: string }>(
48  dinozaury,
49  dino => ({
50    nazwa: dino.nazwa,
51    typ: dino.drapieżnik ? "drapieżnik" : "roślinożerca"
52  })
53);
54
55console.log("\nEtykiety dinozaurów:");
56etykietyDinozaurów.forEach(etykieta => console.log(`- ${etykieta.nazwa}: ${etykieta.typ}`));

Generyczne klasy i metody

Nie tylko funkcje mogą być generyczne - klasy i ich metody również mogą wykorzystywać tę potężną funkcjonalność:

1// Generyczna klasa Kolekcja - może przechowywać dane dowolnego typu
2class Kolekcja<T> {
3  private elementy: T[] = [];
4
5  // Metoda dodająca element do kolekcji
6  dodaj(element: T): void {
7    this.elementy.push(element);
8  }
9
10  // Metoda pobierająca element o danym indeksie
11  pobierz(indeks: number): T | undefined {
12    if (indeks >= 0 && indeks < this.elementy.length) {
13      return this.elementy[indeks];
14    }
15    return undefined;
16  }
17
18  // Metoda zwracająca wszystkie elementy
19  wszystkie(): T[] {
20    return [...this.elementy];
21  }
22
23  // Metoda filtrująca elementy
24  filtruj(predykat: (element: T) => boolean): T[] {
25    return this.elementy.filter(predykat);
26  }
27
28  // Generyczna metoda mapująca do innego typu
29  mapuj<U>(transformacja: (element: T) => U): U[] {
30    return this.elementy.map(transformacja);
31  }
32
33  // Metoda zwracająca liczbę elementów
34  liczbaElementów(): number {
35    return this.elementy.length;
36  }
37}
38
39// Przykład użycia z dinozaurami
40const kolekcjaDinozaurów = new Kolekcja<KrótkaInformacjaDinozaura>();
41
42// Dodawanie dinozaurów do kolekcji
43dinozaury.forEach(dino => kolekcjaDinozaurów.dodaj(dino));
44
45// Pobieranie konkretnego dinozaura
46const pierwszy = kolekcjaDinozaurów.pobierz(0);
47if (pierwszy) {
48  console.log(`Pierwszy dinozaur: ${pierwszy.nazwa} (${pierwszy.gatunek})`);
49}
50
51// Filtrowanie kolekcji
52const małeDinozaury = kolekcjaDinozaurów.filtruj(dino => dino.waga < 1000);
53console.log(`Liczba małych dinozaurów: ${małeDinozaury.length}`);
54
55// Mapowanie do raportów zdrowia
56interface RaportZdrowia {
57  id: string;
58  nazwa: string;
59  statusZdrowia: string;
60  uwagi: string;
61}
62
63const raportyZdrowia = kolekcjaDinozaurów.mapuj<RaportZdrowia>(dino => ({
64  id: dino.id,
65  nazwa: dino.nazwa,
66  statusZdrowia: "dobry", // Symulacja - w rzeczywistości pobieralibyśmy dane z bazy
67  uwagi: dino.drapieżnik
68    ? "Zachować szczególną ostrożność"
69    : "Standardowe procedury"
70}));
71
72console.log("\nRaporty zdrowia:");
73raportyZdrowia.forEach(raport => {
74  console.log(`- ${raport.nazwa}: ${raport.statusZdrowia} (${raport.uwagi})`);
75});

Generyczne interfejsy i typy

Podobnie jak funkcje i klasy, również interfejsy i typy mogą być generyczne:

1// Generyczny interfejs dla systemu przechowywania
2interface System<T> {
3  dodaj(element: T): void;
4  pobierz(id: string): T | null;
5  aktualizuj(id: string, element: T): boolean;
6  usuń(id: string): boolean;
7  wszystkie(): T[];
8}
9
10// Implementacja generycznego systemu przechowywania dla dinozaurów
11class BazaDanychDinozaurów<T extends { id: string }> implements System<T> {
12  private dane: Map<string, T> = new Map();
13
14  dodaj(element: T): void {
15    this.dane.set(element.id, element);
16  }
17
18  pobierz(id: string): T | null {
19    return this.dane.has(id) ? this.dane.get(id)! : null;
20  }
21
22  aktualizuj(id: string, element: T): boolean {
23    if (this.dane.has(id)) {
24      this.dane.set(id, element);
25      return true;
26    }
27    return false;
28  }
29
30  usuń(id: string): boolean {
31    return this.dane.delete(id);
32  }
33
34  wszystkie(): T[] {
35    return Array.from(this.dane.values());
36  }
37}
38
39// Definiowanie typów z użyciem generycznego typu
40type DaneZPrzypisanym<T> = {
41  dane: T;
42  przypisanyPracownik: string;
43  ostatniaAktualizacja: Date;
44};
45
46// Użycie generycznego typu
47type DinozaurZOpiekunem = DaneZPrzypisanym<KrótkaInformacjaDinozaura>;
48
49// Tworzenie bazy danych dla dinozaurów z przypisanymi opiekunami
50const bazaDanychZOpiekunami = new BazaDanychDinozaurów<DinozaurZOpiekunem>();
51
52// Dodawanie rekordów
53bazaDanychZOpiekunami.dodaj({
54  dane: dinozaury[0],
55  przypisanyPracownik: "Owen Grady",
56  ostatniaAktualizacja: new Date(),
57  id: dinozaury[0].id
58});
59
60bazaDanychZOpiekunami.dodaj({
61  dane: dinozaury[1],
62  przypisanyPracownik: "Dr. Henry Wu",
63  ostatniaAktualizacja: new Date(),
64  id: dinozaury[1].id
65});
66
67// Pobieranie danych
68const rexZOpiekunem = bazaDanychZOpiekunami.pobierz("TREX-001");
69if (rexZOpiekunem) {
70  console.log(`\nDinozaur ${rexZOpiekunem.dane.nazwa} jest pod opieką ${rexZOpiekunem.przypisanyPracownik}`);
71  console.log(`Ostatnia aktualizacja: ${rexZOpiekunem.ostatniaAktualizacja.toLocaleDateString()}`);
72}

Praktyczny przykład: System zarządzania parkiem

Przyjrzyjmy się kompleksowemu przykładowi, jak funkcje generyczne mogą być wykorzystane w systemie zarządzania Parkiem Jurajskim:

1// ===== SYSTEM ZARZĄDZANIA PARKIEM JURAJSKIM =====
2
3// Podstawowe interfejsy
4interface ElementParku {
5  id: string;
6  nazwa: string;
7  typElementu: string;
8}
9
10interface ObszarParku extends ElementParku {
11  typElementu: "obszar";
12  współrzędne: [number, number];
13  powierzchnia: number; // w hektarach
14  poziomBezpieczeństwa: 1 | 2 | 3 | 4 | 5;
15}
16
17interface WybiegDinozaurów extends ObszarParku {
18  typElementu: "wybieg";
19  mieszkańcy: string[]; // ID dinozaurów
20  typŚrodowiska: "las" | "sawanna" | "bagno" | "wybrzeże" | "akwen";
21  statusOgrodzenia: "aktywne" | "nieaktywne" | "w naprawie";
22}
23
24interface DinozaurParku extends ElementParku {
25  typElementu: "dinozaur";
26  gatunek: string;
27  wiek: number;
28  waga: number;
29  drapieżnik: boolean;
30  lokalizacja: string; // ID obszaru
31  statusZdrowia: "doskonały" | "dobry" | "wymaga uwagi" | "chory" | "krytyczny";
32}
33
34interface PracownikParku extends ElementParku {
35  typElementu: "pracownik";
36  stanowisko: string;
37  poziomDostępu: 1 | 2 | 3 | 4 | 5;
38  specjalizacja: string[];
39}
40
41// Generyczna klasa repozytorium
42class Repozytorium<T extends ElementParku> {
43  private elementy: Map<string, T> = new Map();
44
45  dodaj(element: T): void {
46    this.elementy.set(element.id, element);
47  }
48
49  pobierz(id: string): T | undefined {
50    return this.elementy.get(id);
51  }
52
53  wszystkie(): T[] {
54    return Array.from(this.elementy.values());
55  }
56
57  aktualizuj(id: string, dane: Partial<T>): boolean {
58    if (this.elementy.has(id)) {
59      const element = this.elementy.get(id)!;
60      this.elementy.set(id, { ...element, ...dane });
61      return true;
62    }
63    return false;
64  }
65
66  usuń(id: string): boolean {
67    return this.elementy.delete(id);
68  }
69
70  filtruj(predykat: (element: T) => boolean): T[] {
71    return this.wszystkie().filter(predykat);
72  }
73}
74
75// Generyczna funkcja do znajdowania powiązań między elementami
76function znajdźPowiązania<T extends ElementParku, U extends ElementParku>(
77  źródła: T[],
78  cele: U[],
79  predykat: (źródło: T, cel: U) => boolean
80): Array<[T, U[]]> {
81  return źródła.map(źródło => {
82    const powiązaneCele = cele.filter(cel => predykat(źródło, cel));
83    return [źródło, powiązaneCele] as [T, U[]];
84  });
85}
86
87// Generyczna funkcja do tworzenia raportów
88function generujRaport<T extends ElementParku>(
89  nazwa: string,
90  elementy: T[],
91  formatujElement: (element: T) => string
92): string {
93  let raport = `=== ${nazwa} ===\n`;
94  raport += `Data wygenerowania: ${new Date().toLocaleString()}\n`;
95  raport += `Liczba elementów: ${elementy.length}\n\n`;
96
97  if (elementy.length === 0) {
98    raport += "Brak danych do wyświetlenia.\n";
99  } else {
100    elementy.forEach(element => {
101      raport += formatujElement(element) + "\n";
102    });
103  }
104
105  return raport;
106}
107
108// Symulacja danych parku
109// Tworzenie repozytoriów
110const repoDinozaurów = new Repozytorium<DinozaurParku>();
111const repoObszarów = new Repozytorium<ObszarParku>();
112const repoWybiegów = new Repozytorium<WybiegDinozaurów>();
113const repoPracowników = new Repozytorium<PracownikParku>();
114
115// Dodawanie danych do repozytoriów
116// Obszary
117const obszarCentralny: ObszarParku = {
118  id: "OBS-001",
119  nazwa: "Obszar Centralny",
120  typElementu: "obszar",
121  współrzędne: [20.123, -75.456],
122  powierzchnia: 25,
123  poziomBezpieczeństwa: 1
124};
125repoObszarów.dodaj(obszarCentralny);
126
127// Wybiegi
128const wybiegTRex: WybiegDinozaurów = {
129  id: "WYB-001",
130  nazwa: "Wybieg T-Rex",
131  typElementu: "wybieg",
132  współrzędne: [20.145, -75.460],
133  powierzchnia: 8,
134  poziomBezpieczeństwa: 5,
135  mieszkańcy: ["DINO-001"],
136  typŚrodowiska: "las",
137  statusOgrodzenia: "aktywne"
138};
139repoWybiegów.dodaj(wybiegTRex);
140
141const wybiegRaptory: WybiegDinozaurów = {
142  id: "WYB-002",
143  nazwa: "Wybieg Raptorów",
144  typElementu: "wybieg",
145  współrzędne: [20.150, -75.465],
146  powierzchnia: 5,
147  poziomBezpieczeństwa: 5,
148  mieszkańcy: ["DINO-002", "DINO-003", "DINO-004", "DINO-005"],
149  typŚrodowiska: "sawanna",
150  statusOgrodzenia: "aktywne"
151};
152repoWybiegów.dodaj(wybiegRaptory);
153
154// Dinozaury
155const trex: DinozaurParku = {
156  id: "DINO-001",
157  nazwa: "Rexy",
158  typElementu: "dinozaur",
159  gatunek: "Tyrannosaurus Rex",
160  wiek: 7,
161  waga: 8000,
162  drapieżnik: true,
163  lokalizacja: "WYB-001",
164  statusZdrowia: "dobry"
165};
166repoDinozaurów.dodaj(trex);
167
168const raptor1: DinozaurParku = {
169  id: "DINO-002",
170  nazwa: "Blue",
171  typElementu: "dinozaur",
172  gatunek: "Velociraptor",
173  wiek: 4,
174  waga: 150,
175  drapieżnik: true,
176  lokalizacja: "WYB-002",
177  statusZdrowia: "doskonały"
178};
179repoDinozaurów.dodaj(raptor1);
180
181// Pracownicy
182const trener: PracownikParku = {
183  id: "PRAC-001",
184  nazwa: "Owen Grady",
185  typElementu: "pracownik",
186  stanowisko: "Treser Raptorów",
187  poziomDostępu: 4,
188  specjalizacja: ["Behawioryzm", "Tresura dinozaurów", "Operacje terenowe"]
189};
190repoPracowników.dodaj(trener);
191
192const naukowiec: PracownikParku = {
193  id: "PRAC-002",
194  nazwa: "Dr. Henry Wu",
195  typElementu: "pracownik",
196  stanowisko: "Główny Genetyk",
197  poziomDostępu: 5,
198  specjalizacja: ["Genetyka", "Inżynieria genetyczna", "Klonowanie"]
199};
200repoPracowników.dodaj(naukowiec);
201
202// Przykładowe użycie generycznych funkcji
203
204// 1. Znajdowanie dinozaurów w określonych wybiegach
205const dinozauryWWybiegach = znajdźPowiązania<WybiegDinozaurów, DinozaurParku>(
206  repoWybiegów.wszystkie(),
207  repoDinozaurów.wszystkie(),
208  (wybieg, dino) => wybieg.mieszkańcy.includes(dino.id)
209);
210
211// 2. Generowanie raportu o stanie wybiegów
212const raportWybiegów = generujRaport<WybiegDinozaurów>(
213  "RAPORT STANU WYBIEGÓW",
214  repoWybiegów.wszystkie(),
215  wybieg => {
216    const liczbaMieszkańców = wybieg.mieszkańcy.length;
217    return `${wybieg.nazwa} (ID: ${wybieg.id})
218    - Typ środowiska: ${wybieg.typŚrodowiska}
219    - Powierzchnia: ${wybieg.powierzchnia} ha
220    - Status ogrodzenia: ${wybieg.statusOgrodzenia}
221    - Poziom bezpieczeństwa: ${wybieg.poziomBezpieczeństwa}
222    - Liczba mieszkańców: ${liczbaMieszkańców}`;
223  }
224);
225
226// 3. Generowanie raportu o stanie zdrowia dinozaurów
227const raportZdrowiaDinozaurów = generujRaport<DinozaurParku>(
228  "RAPORT ZDROWIA DINOZAURÓW",
229  repoDinozaurów.wszystkie(),
230  dino => {
231    const lokalizacja = repoWybiegów.pobierz(dino.lokalizacja)?.nazwa || "Nieznana";
232    return `${dino.nazwa} (${dino.gatunek})
233    - ID: ${dino.id}
234    - Wiek: ${dino.wiek} lat
235    - Waga: ${dino.waga} kg
236    - Status zdrowia: ${dino.statusZdrowia}
237    - Lokalizacja: ${lokalizacja}`;
238  }
239);
240
241// Wyświetlanie raportów
242console.log("=== DINOZAURY W WYBIEGACH ===");
243dinozauryWWybiegach.forEach(([wybieg, dinozaury]) => {
244  console.log(`Wybieg: ${wybieg.nazwa}`);
245  if (dinozaury.length === 0) {
246    console.log("  Brak mieszkańców");
247  } else {
248    dinozaury.forEach(dino => {
249      console.log(`  - ${dino.nazwa} (${dino.gatunek}): ${dino.statusZdrowia}`);
250    });
251  }
252});
253
254console.log("\n" + raportWybiegów);
255console.log("\n" + raportZdrowiaDinozaurów);

Podsumowanie

Funkcje generyczne to niezwykle potężne narzędzie w TypeScript, które pozwala na tworzenie elastycznych, wielokrotnego użytku komponentów, zachowując jednocześnie pełne bezpieczeństwo typów. Podobnie jak zaawansowane techniki genetyczne Dr. Wu, funkcje generyczne pozwalają na tworzenie wysoce adaptacyjnych rozwiązań, które mogą działać z różnymi typami danych.

TechnikaZastosowanie w Parku Jurajskim
Podstawowe funkcje generyczneObsługa różnych typów danych dinozaurów
Wieloparametrowe funkcje generyczneŁączenie danych o dinozaurach z różnych źródeł
Ograniczenia generyczneZapewnienie, że funkcje działają tylko z odpowiednimi typami dinozaurów
Ograniczenia z keyofBezpieczny dostęp do właściwości dinozaurów
Domyślne parametry typuUproszczenie pracy z najczęściej używanymi typami danych
Generyczne operacje na kolekcjachEfektywne zarządzanie grupami dinozaurów
Generyczne klasy i metodyTworzenie elastycznych systemów zarządzania okazami
Generyczne interfejsy i typyDefiniowanie spójnych kontraktów dla różnych podsystemów parku

Dr. Wu podsumowuje: "Tak jak nasze przełomowe techniki genetyczne pozwoliły nam przywrócić do życia gatunki, które zniknęły z Ziemi 65 milionów lat temu, tak generyczne funkcje TypeScript pozwalają programistom tworzyć elastyczne, adaptacyjne rozwiązania, które mogą ewoluować wraz z potrzebami systemu. Pamiętaj jednak, że z wielką mocą wiąże się wielka odpowiedzialność - zarówno w inżynierii genetycznej, jak i w programowaniu."

Kod do tej lekcji: index.ts
1// Funkcje generyczne w Parku Jurajskim
2console.log("Funkcje generyczne TypeScript\n");
3
4// ===========================================
5// 1. Podstawowe funkcje generyczne
6// ===========================================
7console.log("=== PODSTAWOWE GENERICS ===");
8
9// Prosta funkcja generyczna
10function identity<T>(value: T): T {
11  return value;
12}
13
14const stringValue = identity<string>("Tyrannosaurus Rex");
15const numberValue = identity<number>(8000);
16const boolValue = identity<boolean>(true);
17
18console.log("String:", stringValue);
19console.log("Number:", numberValue);
20console.log("Boolean:", boolValue);
21
22// Type inference - TypeScript wywnioskuje typ
23const autoString = identity("Auto string"); // T = string
24const autoNumber = identity(42); // T = number
25
26console.log("Auto string:", autoString);
27console.log("Auto number:", autoNumber);
28
29// ===========================================
30// 2. Generics z tablicami
31// ===========================================
32console.log("\n=== GENERICS Z TABLICAMI ===");
33
34function getFirst<T>(array: T[]): T | undefined {
35  return array[0];
36}
37
38const dinosaurs = ["T-Rex", "Velociraptor", "Triceratops"];
39const weights = [8000, 350, 5000];
40
41console.log("First dinosaur:", getFirst(dinosaurs));
42console.log("First weight:", getFirst(weights));
43
44function getLast<T>(array: T[]): T | undefined {
45  return array[array.length - 1];
46}
47
48console.log("Last dinosaur:", getLast(dinosaurs));
49
50// ===========================================
51// 3. Generics z wieloma typami
52// ===========================================
53console.log("\n=== WIELE TYPÓW GENERYCZNYCH ===");
54
55function createPair<K, V>(key: K, value: V): { key: K; value: V } {
56  return { key, value };
57}
58
59const pair1 = createPair<string, number>("age", 5);
60const pair2 = createPair<number, string>(1, "Rexy");
61const pair3 = createPair("weight", 8000); // type inference
62
63console.log("Pair 1:", pair1);
64console.log("Pair 2:", pair2);
65console.log("Pair 3:", pair3);
66
67// ===========================================
68// 4. Generic constraints
69// ===========================================
70console.log("\n=== GENERIC CONSTRAINTS ===");
71
72interface HasName {
73  name: string;
74}
75
76// T musi mieć właściwość 'name'
77function logName<T extends HasName>(entity: T): T {
78  console.log(`Name: ${entity.name}`);
79  return entity;
80}
81
82interface Dinosaur {
83  name: string;
84  species: string;
85}
86
87const trex: Dinosaur = {
88  name: "Rexy",
89  species: "Tyrannosaurus Rex"
90};
91
92logName(trex);
93// logName({ age: 5 }); // Error: no 'name' property
94
95// Constraint z extends
96function getLength<T extends { length: number }>(value: T): number {
97  return value.length;
98}
99
100console.log("String length:", getLength("Hello"));
101console.log("Array length:", getLength([1, 2, 3]));
102// console.log(getLength(123)); // Error: no 'length' property
103
104// ===========================================
105// 5. Generic z interfejsami
106// ===========================================
107console.log("\n=== GENERICS Z INTERFEJSAMI ===");
108
109interface Container<T> {
110  value: T;
111  getValue(): T;
112  setValue(value: T): void;
113}
114
115class DinosaurContainer implements Container<Dinosaur> {
116  value: Dinosaur;
117
118  constructor(value: Dinosaur) {
119    this.value = value;
120  }
121
122  getValue(): Dinosaur {
123    return this.value;
124  }
125
126  setValue(value: Dinosaur): void {
127    this.value = value;
128  }
129}
130
131const dinoContainer = new DinosaurContainer(trex);
132console.log("Container value:", dinoContainer.getValue());
133
134// ===========================================
135// 6. Generic Repository Pattern
136// ===========================================
137console.log("\n=== REPOSITORY PATTERN ===");
138
139interface Repository<T> {
140  items: T[];
141  add(item: T): void;
142  remove(item: T): void;
143  find(predicate: (item: T) => boolean): T | undefined;
144  getAll(): T[];
145}
146
147class DinosaurRepository implements Repository<Dinosaur> {
148  items: Dinosaur[] = [];
149
150  add(dinosaur: Dinosaur): void {
151    this.items.push(dinosaur);
152    console.log(`Added ${dinosaur.name} to repository`);
153  }
154
155  remove(dinosaur: Dinosaur): void {
156    const index = this.items.findIndex(d => d.name === dinosaur.name);
157    if (index !== -1) {
158      this.items.splice(index, 1);
159      console.log(`Removed ${dinosaur.name} from repository`);
160    }
161  }
162
163  find(predicate: (dinosaur: Dinosaur) => boolean): Dinosaur | undefined {
164    return this.items.find(predicate);
165  }
166
167  getAll(): Dinosaur[] {
168    return [...this.items];
169  }
170}
171
172const dinoRepo = new DinosaurRepository();
173dinoRepo.add(trex);
174dinoRepo.add({ name: "Blue", species: "Velociraptor" });
175
176const foundDino = dinoRepo.find(d => d.name === "Blue");
177console.log("Found:", foundDino);
178
179console.log("All dinosaurs:", dinoRepo.getAll().map(d => d.name));
180
181// ===========================================
182// 7. Generic funkcje Array
183// ===========================================
184console.log("\n=== GENERIC ARRAY FUNCTIONS ===");
185
186function map<T, U>(array: T[], fn: (item: T) => U): U[] {
187  const result: U[] = [];
188  for (const item of array) {
189    result.push(fn(item));
190  }
191  return result;
192}
193
194const dinoNames = ["Rexy", "Blue", "Charlie"];
195const upperNames = map(dinoNames, name => name.toUpperCase());
196const nameLengths = map(dinoNames, name => name.length);
197
198console.log("Upper names:", upperNames);
199console.log("Name lengths:", nameLengths);
200
201function filter<T>(array: T[], predicate: (item: T) => boolean): T[] {
202  const result: T[] = [];
203  for (const item of array) {
204    if (predicate(item)) {
205      result.push(item);
206    }
207  }
208  return result;
209}
210
211const longNames = filter(dinoNames, name => name.length > 4);
212console.log("Long names:", longNames);
213
214// ===========================================
215// 8. Generic Promise
216// ===========================================
217console.log("\n=== GENERIC PROMISE ===");
218
219function fetchDinosaur(id: number): Promise<Dinosaur> {
220  return new Promise((resolve) => {
221    setTimeout(() => {
222      resolve({
223        name: `Dinosaur-${id}`,
224        species: "Unknown"
225      });
226    }, 100);
227  });
228}
229
230async function getDinosaurData(): Promise<void> {
231  const dino = await fetchDinosaur(1);
232  console.log("Fetched dinosaur:", dino);
233}
234
235getDinosaurData();
236
237// ===========================================
238// 9. Generic Default Types
239// ===========================================
240console.log("\n=== DEFAULT TYPES ===");
241
242interface Response<T = any> {
243  status: number;
244  data: T;
245  message: string;
246}
247
248const stringResponse: Response<string> = {
249  status: 200,
250  data: "Success",
251  message: "OK"
252};
253
254const dinoResponse: Response<Dinosaur> = {
255  status: 200,
256  data: trex,
257  message: "Dinosaur found"
258};
259
260const defaultResponse: Response = {
261  status: 404,
262  data: null,
263  message: "Not found"
264};
265
266console.log("String response:", stringResponse);
267console.log("Dino response:", dinoResponse.data.name);
268
269// ===========================================
270// 10. Keyof operator z Generic
271// ===========================================
272console.log("\n=== KEYOF OPERATOR ===");
273
274function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
275  return obj[key];
276}
277
278const blue: Dinosaur = {
279  name: "Blue",
280  species: "Velociraptor"
281};
282
283const dinoName = getProperty(blue, "name"); // string
284const dinoSpecies = getProperty(blue, "species"); // string
285// const invalid = getProperty(blue, "age"); // Error
286
287console.log("Property 'name':", dinoName);
288console.log("Property 'species':", dinoSpecies);
289
290// ===========================================
291// 11. Generic Utility Functions
292// ===========================================
293console.log("\n=== UTILITY FUNCTIONS ===");
294
295function merge<T, U>(obj1: T, obj2: U): T & U {
296  return { ...obj1, ...obj2 };
297}
298
299const basicInfo = { name: "Rexy", age: 5 };
300const detailedInfo = { species: "T-Rex", weight: 8000 };
301
302const fullInfo = merge(basicInfo, detailedInfo);
303console.log("Merged:", fullInfo);
304
305function partial<T>(obj: T): Partial<T> {
306  return { ...obj };
307}
308
309const partialDino = partial(trex);
310console.log("Partial:", partialDino);
311
312// ===========================================
313// 12. Podsumowanie
314// ===========================================
315console.log("\n=== PODSUMOWANIE ===");
316console.log("Funkcje generyczne - <T>");
317console.log("Wiele typów - <K, V>");
318console.log("Constraints - <T extends Type>");
319console.log("Generic interfejsy - Interface<T>");
320console.log("Repository pattern - uniwersalne repozytorium");
321console.log("Generic Promise - asynchroniczne typy");
322console.log("Keyof operator - dostęp do kluczy");
323console.log("Utility functions - uniwersalne narzędzia");
324
325console.log("\nFunkcje generyczne opanowane!");

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. Czym jest funkcja generyczna w TypeScript?

  2. 2. Jak ograniczyć parametr generyczny T, aby akceptował tylko typy z właściwością 'id'?

Zadania praktyczne w grze

  • Edytor kodu

    Zaimplementuj generyczną funkcję findById<T> przyjmującą tablicę obiektów z polem id i zwracającą element lub undefined.

  • Edytor kodu

    Zaimplementuj generyczną funkcję filterByProperty<T, K> przyjmującą tablicę, klucz i wartość, zwracającą przefiltrowane elementy.

  • Klikanie w kolejności

    Ułóż elementy generycznej funkcji TypeScript:

  • Układanie w pionie

    Uporządkuj koncepty generyczne od najprostszych do najbardziej zaawansowanych:

Przydatne artykuły