Kurs JavaScript i TypeScript · Moduł 6: Podstawy TypeScript
Funkcje generyczne
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 numberW powyższym przykładzie:
- Definiujemy funkcję generyczną
identycznośćGenetycznaz parametrem typuT - Funkcja przyjmuje argument typu
Ti zwraca wartość tego samego typu - Używamy funkcji z różnymi typami: string, number, boolean
- 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:
- Definiujemy interfejs
PodstawoweInformacjeDinozauraz minimalnymi wymaganiami - Tworzymy funkcję generyczną, która działa tylko z typami rozszerzającymi ten interfejs
- Funkcja może bezpiecznie korzystać z właściwości wymaganych przez interfejs
- 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:
- Tworzymy funkcję
pobierzWłaściwośćDinozaura, która przyjmuje obiekt typuTi kluczK, ograniczony do kluczyT - Funkcja zwraca wartość właściwości o danym kluczu, a TypeScript wie dokładnie, jakiego typu będzie zwrócona wartość
- 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.
| Technika | Zastosowanie w Parku Jurajskim |
|---|---|
| Podstawowe funkcje generyczne | Obsługa różnych typów danych dinozaurów |
| Wieloparametrowe funkcje generyczne | Łączenie danych o dinozaurach z różnych źródeł |
| Ograniczenia generyczne | Zapewnienie, że funkcje działają tylko z odpowiednimi typami dinozaurów |
Ograniczenia z keyof | Bezpieczny dostęp do właściwości dinozaurów |
| Domyślne parametry typu | Uproszczenie pracy z najczęściej używanymi typami danych |
| Generyczne operacje na kolekcjach | Efektywne zarządzanie grupami dinozaurów |
| Generyczne klasy i metody | Tworzenie elastycznych systemów zarządzania okazami |
| Generyczne interfejsy i typy | Definiowanie 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. Czym jest funkcja generyczna w TypeScript?
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: