Kurs JavaScript i React · Moduł 12: React i TypeScript
Komponenty Generyczne - Uniwersalne Systemy Nawigacji
W tej lekcji5
Masz komponent listy planet. Po tygodniu potrzebujesz listy gwiazd, potem członków załogi, potem statków. Kopiujesz komponent czwarty raz i zmieniasz tylko typ - a każdy błąd trzeba odtąd poprawiać w czterech miejscach. Wyobraź sobie zamiast tego uniwersalny system nawigacji na statku kosmicznym, który może wyświetlać dane o planetach, gwiazdach, statkach - cokolwiek mu podasz. W TypeScript takie "uniwersalne" komponenty tworzymy dzięki generykom.
Czym są generyki?
Generyki to parametry typów - pozwalają tworzyć komponenty i funkcje, które działają z różnymi typami danych, zachowując pełną kontrolę typów. Parametr typu zapisujesz w nawiasach ostrych, zwykle jedną wielką literą T (od "type"). To miejsce na typ, który zostanie podany dopiero przy użyciu - jak zmienna, tylko dla typów.
W przykładzie używamy interfejsów Planet i Star z lekcji o props, a planets i stars to tablice takich obiektów:
1// Bez generyków - trzeba pisać osobne funkcje
2function getFirstPlanet(items: Planet[]): Planet {
3 return items[0];
4}
5function getFirstStar(items: Star[]): Star {
6 return items[0];
7}Obie funkcje robią dokładnie to samo, różnią się tylko typem. Generyk zamienia je w jedną:
1// Z generykami - jedna funkcja dla wszystkich typów
2function getFirst<T>(items: T[]): T {
3 return items[0];
4}
5
6const planet = getFirst<Planet>(planets); // zwraca Planet
7const star = getFirst<Star>(stars); // zwraca Star
8const name = getFirst<string>(["a", "b"]); // zwraca stringZapis getFirst<T>(items: T[]): T czytamy: "dla dowolnego typu T przyjmij tablicę T i zwróć jedno T". Typ nie ginie po drodze - planet ma typ Planet, więc edytor podpowie jego pola. W praktyce nawiasy przy wywołaniu możesz pominąć, bo TypeScript wydedukuje T z argumentu: getFirst(planets) da ten sam wynik.
Generyczny komponent listy
Jednym z najczęstszych zastosowań generyków w React jest uniwersalny komponent listy. Lista potrzebuje jednej gwarancji: każdy element musi mieć id, bo React wymaga unikalnego key. Taką gwarancję zapisujemy słowem extends - zapis T extends HasId oznacza "T może być czymkolwiek, byle miało pole id". To tak zwane ograniczenie (constraint).
1// Każdy element musi mieć id do użycia jako key
2interface HasId {
3 id: number | string;
4}
5
6interface ListProps<T extends HasId> {
7 items: T[];
8 renderItem: (item: T) => React.ReactNode;
9 emptyMessage?: string;
10}Props renderItem to tak zwany render prop: funkcja, którą rodzic przekazuje, aby zdecydować, jak narysować pojedynczy element. Sama lista nie wie nic o planetach - wie tylko, jak je ułożyć:
1function List<T extends HasId>({ items, renderItem, emptyMessage = "Brak danych" }: ListProps<T>) {
2 if (items.length === 0) {
3 return <p>{emptyMessage}</p>;
4 }
5
6 return (
7 <ul>
8 {items.map(item => (
9 <li key={item.id}>{renderItem(item)}</li>
10 ))}
11 </ul>
12 );
13}Dzięki ograniczeniu item.id jest dozwolone w key. Gdybyś spróbował odczytać item.name, TypeScript zaprotestuje - lista nie ma gwarancji, że takie pole istnieje. Uwaga techniczna: w pliku .tsx funkcja strzałkowa z generykiem wymaga zapisu <T,> z przecinkiem, żeby kompilator nie pomylił jej ze znacznikiem JSX. Deklaracja function tego problemu nie ma.
Użycie generycznego komponentu
Przygotujmy dwa różne rodzaje danych. Planety mają id liczbowe, członkowie załogi - tekstowe, i oba warianty spełniają HasId:
1interface Planet {
2 id: number;
3 name: string;
4 type: string;
5}
6
7interface CrewMember {
8 id: string;
9 name: string;
10 rank: string;
11}Teraz ten sam komponent List wyświetla obie kolekcje. Zapis <List<Planet> jawnie podaje typ, choć TypeScript wydedukowałby go z items:
1function Dashboard() {
2 const planets: Planet[] = [
3 { id: 1, name: "Mars", type: "skalista" },
4 { id: 2, name: "Jupiter", type: "gazowy" }
5 ];
6
7 const crew: CrewMember[] = [
8 { id: "c1", name: "Nova", rank: "Kapitan" },
9 { id: "c2", name: "Ra", rank: "Nawigator" }
10 ];
11
12 return (
13 <div>
14 <h2>Planety</h2>
15 <List<Planet>
16 items={planets}
17 renderItem={(planet) => <span>{planet.name} ({planet.type})</span>}
18 />
19
20 <h2>Załoga</h2>
21 <List<CrewMember>
22 items={crew}
23 renderItem={(member) => <span>{member.name} - {member.rank}</span>}
24 />
25 </div>
26 );
27}W renderItem parametr planet ma typ Planet, a member - CrewMember, bez żadnych adnotacji. Komponent List nie zmienił się ani o linijkę.
Generyczny komponent Select
Ten sam wzorzec działa dla pola wyboru. Ograniczenie wymaga pól value i label, a onChange oddaje rodzicowi cały wybrany obiekt, z zachowaniem jego dokładnego typu. Handler zmiany korzysta z ChangeEvent z poprzedniej lekcji:
1interface SelectOption {
2 value: string;
3 label: string;
4}
5
6interface SelectProps<T extends SelectOption> {
7 options: T[];
8 value: string;
9 onChange: (option: T) => void;
10 placeholder?: string;
11}
12
13function Select<T extends SelectOption>({
14 options, value, onChange, placeholder
15}: SelectProps<T>) {
16 const handleChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
17 const selected = options.find(opt => opt.value === e.target.value);
18 if (selected) onChange(selected);
19 };
20
21 return (
22 <select value={value} onChange={handleChange}>
23 {placeholder && <option value="">{placeholder}</option>}
24 {options.map(opt => (
25 <option key={opt.value} value={opt.value}>
26 {opt.label}
27 </option>
28 ))}
29 </select>
30 );
31}Metoda find może zwrócić undefined, dlatego if (selected) jest konieczne - TypeScript nie pozwoli przekazać możliwego undefined do onChange.
Ograniczenia generyków (constraints)
Słowo kluczowe extends pozwala ograniczyć, jakie typy są akceptowane. Ograniczeniem może być typ zapisany w miejscu albo nazwany interfejs:
1// T musi mieć pole 'name'
2function displayName<T extends { name: string }>(item: T): string {
3 return item.name;
4}
5
6// T musi implementować interfejs Sortable
7interface Sortable {
8 sortOrder: number;
9}
10
11function sortItems<T extends Sortable>(items: T[]): T[] {
12 return [...items].sort((a, b) => a.sortOrder - b.sortOrder);
13}Ograniczenie to minimum, nie dokładny kształt: obiekt może mieć dodatkowe pola i zostaną one zachowane w typie wyniku. [...items] tworzy kopię, więc sortowanie nie modyfikuje oryginalnej tablicy. Moja rada: sięgaj po generyk dopiero wtedy, gdy ten sam komponent naprawdę obsługuje dwa różne typy danych - wcześniej wystarczy zwykły interfejs. W projekcie końcowym zbudujesz generyczną tabelę danych.
Kod do tej lekcji: App.tsx
1import React, { useState } from 'react';
2
3// Ograniczenie generyczne - kazdy element musi miec id
4interface HasId {
5 id: number | string;
6}
7
8// Generyczny komponent listy
9interface ListProps<T extends HasId> {
10 items: T[];
11 renderItem: (item: T) => React.ReactNode;
12 title: string;
13 emptyMessage?: string;
14}
15
16function List<T extends HasId>({ items, renderItem, title, emptyMessage = "Brak elementow" }: ListProps<T>) {
17 return (
18 <div className="list-container">
19 <h2>{title} ({items.length})</h2>
20 {items.length === 0 ? (
21 <p className="empty">{emptyMessage}</p>
22 ) : (
23 <ul className="list">
24 {items.map(item => (
25 <li key={item.id} className="list-item">{renderItem(item)}</li>
26 ))}
27 </ul>
28 )}
29 </div>
30 );
31}
32
33// Rozne typy danych
34interface Planet {
35 id: number;
36 name: string;
37 type: string;
38 habitable: boolean;
39}
40
41interface CrewMember {
42 id: string;
43 name: string;
44 rank: string;
45 specialization: string;
46}
47
48interface Mission {
49 id: number;
50 codeName: string;
51 status: "planned" | "active" | "completed";
52 destination: string;
53}
54
55function App() {
56 const [planets] = useState<Planet[]>([
57 { id: 1, name: "Kepler-442b", type: "skalista", habitable: true },
58 { id: 2, name: "HD 209458 b", type: "gazowa", habitable: false },
59 { id: 3, name: "Proxima Centauri b", type: "skalista", habitable: true },
60 ]);
61
62 const [crew] = useState<CrewMember[]>([
63 { id: "c1", name: "Nova", rank: "Kapitan", specialization: "Nawigacja" },
64 { id: "c2", name: "Ra", rank: "Nawigator", specialization: "Astrofizyka" },
65 { id: "c3", name: "Astro", rank: "Inzynier", specialization: "Silniki" },
66 ]);
67
68 const [missions] = useState<Mission[]>([
69 { id: 1, codeName: "DAWN", status: "active", destination: "Mars" },
70 { id: 2, codeName: "HORIZON", status: "planned", destination: "Jupiter" },
71 ]);
72
73 const statusColors = { planned: "#f59e0b", active: "#22c55e", completed: "#6b7280" };
74
75 return (
76 <div className="app">
77 <h1>Generyczne Komponenty</h1>
78
79 <List<Planet>
80 title="Planety"
81 items={planets}
82 renderItem={(planet) => (
83 <div className="item-content">
84 <strong>{planet.name}</strong>
85 <span className="detail">{planet.type}</span>
86 <span className={"badge " + (planet.habitable ? "green" : "red")}>
87 {planet.habitable ? "Zamieszkiwalna" : "Niezamieszkiwalna"}
88 </span>
89 </div>
90 )}
91 />
92
93 <List<CrewMember>
94 title="Zaloga"
95 items={crew}
96 renderItem={(member) => (
97 <div className="item-content">
98 <strong>{member.name}</strong>
99 <span className="detail">{member.rank}</span>
100 <span className="spec">{member.specialization}</span>
101 </div>
102 )}
103 />
104
105 <List<Mission>
106 title="Misje"
107 items={missions}
108 renderItem={(mission) => (
109 <div className="item-content">
110 <strong>{mission.codeName}</strong>
111 <span className="detail">{mission.destination}</span>
112 <span style={{ color: statusColors[mission.status] }}>
113 {mission.status.toUpperCase()}
114 </span>
115 </div>
116 )}
117 />
118 </div>
119 );
120}
121
122export default App;Pamiętaj: generyk to uniwersalne dokowanie - każdy statek, który spełnia wymagania portu, zadokuje bez przebudowy stacji.
Widzisz błąd w tej lekcji?
Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. Co oznacza zapis <T> w deklaracji function getFirst<T>(items: T[]): T?
2. Co oznacza zapis <T extends { id: number }> w generycznym komponencie React?
Zadania praktyczne w grze
- Układanie w pionie
Ułóż kolejność kroków tworzenia generycznego komponentu listy:
- Edytor kodu
Generyczny komponent Select z ograniczeniami
- Układanie w poziomie
Ułóż składnię ograniczenia generycznego (constraint) w TypeScript:
- Edytor kodu
Generyczny komponent z render prop i filtrowaniem