Kurs JavaScript i TypeScript · Moduł 6: Podstawy TypeScript
Typy narzędziowe TypeScript (Utility Types)
W tej lekcji10
Dr. Henry Wu przegląda bazę danych genetycznych Parku Jurajskiego. "Mamy interfejs Dinosaur z 15 właściwościami," mówi, "ale do formularza aktualizacji potrzebuję tylko 5, a do podglądu zaledwie 3. Czy muszę tworzyć osobny interfejs dla każdego przypadku?" Kopiowanie interfejsów ma ukryty koszt: gdy do Dinosaur dojdzie nowe pole, każdą kopię trzeba poprawić ręcznie, a o jednej zawsze ktoś zapomni. Na szczęście TypeScript oferuje typy narzędziowe (Utility Types), czyli gotowe transformacje, które tworzą nowe typy na podstawie istniejących.
Czym są typy narzędziowe?
Typy narzędziowe to wbudowane w TypeScript typy generyczne, które przekształcają istniejące typy w nowe. Typ do przekształcenia podajesz w nawiasach ostrych, jak argument funkcji generycznej z poprzednich lekcji. Nie trzeba ich importować, a po kompilacji znikają, bo istnieją wyłącznie na etapie sprawdzania typów. Zamiast osobnych interfejsów masz jeden typ bazowy, z którego wyprowadzasz warianty, jak kolejne linie hodowlane z jednej próbki DNA.
Partial<T> - wszystkie właściwości opcjonalne
W przykładach użyjemy uproszczonego interfejsu z pięcioma polami. Pole dietType przyjmuje jedną z trzech wartości zapisanych jako unia literałów. Partial<T> sprawia, że wszystkie właściwości typu T stają się opcjonalne. To idealny typ dla aktualizacji, w której nie chcemy wymagać wszystkich pól:
1interface Dinosaur {
2 name: string;
3 species: string;
4 dangerLevel: number;
5 enclosureId: string;
6 dietType: 'herbivore' | 'carnivore' | 'omnivore';
7}
8
9// Partial<Dinosaur> = wszystkie pola opcjonalne
10type DinosaurUpdate = Partial<Dinosaur>;
11
12// Teraz możemy przekazać tylko zmienione pola
13function updateDinosaur(id: string, changes: DinosaurUpdate): void {
14 console.log(`Aktualizuję dinozaura ${id}:`, changes);
15}
16
17// Poprawne - przekazujemy tylko to, co zmieniamy
18updateDinosaur('DINO-001', { dangerLevel: 9 });
19updateDinosaur('DINO-002', { name: 'Blue', enclosureId: 'E-7' });Sam interfejs Dinosaur się nie zmienił, bo Partial tworzy nowy typ obok niego. Kontrola nadal działa: literówka w rodzaju dangerLvl zostanie zgłoszona. Partial jest płytki, więc pola zagnieżdżonego obiektu pozostają wymagane.
Required<T> - wszystkie właściwości wymagane
Required<T> działa odwrotnie niż Partial: usuwa znak zapytania, więc wszystkie właściwości, nawet opcjonalne, stają się wymagane:
1interface DinosaurConfig {
2 name: string;
3 trackingChip?: boolean; // opcjonalne
4 feedingSchedule?: string; // opcjonalne
5}
6
7// Required<DinosaurConfig> = wszystko wymagane
8type CompleteDinosaurConfig = Required<DinosaurConfig>;
9
10// Teraz trackingChip i feedingSchedule też są wymagane
11const config: CompleteDinosaurConfig = {
12 name: 'Rex',
13 trackingChip: true, // musi być podane
14 feedingSchedule: '08:00' // musi być podane
15};Gdyby zabrakło feedingSchedule, kompilator zgłosi brak właściwości. Przydaje się to wtedy, gdy konfiguracja po uzupełnieniu wartości domyślnych musi już być kompletna.
Pick<T, K> - wybierz właściwości
Pick<T, K> tworzy nowy typ zawierający tylko wybrane właściwości z T. Drugi parametr to nazwy pól połączone znakiem |:
1// Tylko name i species z pełnego interfejsu Dinosaur
2type DinosaurPreview = Pick<Dinosaur, 'name' | 'species'>;
3
4// DinosaurPreview = { name: string; species: string; }
5
6const preview: DinosaurPreview = {
7 name: 'Blue',
8 species: 'Velociraptor'
9};
10
11// Przydatne np. do listy wyświetlanej na ekranie
12function renderDinosaurList(dinosaurs: DinosaurPreview[]): void {
13 dinosaurs.forEach(dino => {
14 console.log(`${dino.name} (${dino.species})`);
15 });
16}Pick sprawdza, czy podane klucze istnieją: Pick<Dinosaur, 'enclosureID'> z literówką od razu skończy się błędem kompilacji. Typy pól przechodzą bez zmian, więc name nadal jest stringiem.
Omit<T, K> - pomiń właściwości
Omit<T, K> działa odwrotnie niż Pick i tworzy nowy typ bez wybranych właściwości:
1// Dinosaur bez enclosureId (np. dla nowo odkrytego gatunku)
2type DinosaurWithoutEnclosure = Omit<Dinosaur, 'enclosureId'>;
3
4const newDiscovery: DinosaurWithoutEnclosure = {
5 name: 'Nowy gatunek',
6 species: 'Unknownus rex',
7 dangerLevel: 5,
8 dietType: 'carnivore'
9 // nie musimy podawać enclosureId
10};
11
12// Omit wielu pól naraz
13type DinosaurBasicInfo = Omit<Dinosaur, 'enclosureId' | 'dangerLevel'>;Omit nie usuwa niczego z istniejących obiektów, tylko opisuje nowy kształt. Ma też pułapkę: w przeciwieństwie do Pick nie sprawdza, czy klucz istnieje, więc Omit<Dinosaur, 'enclosureID'> przejdzie bez błędu i nic nie pominie. Dlatego gdy zostawiasz tylko kilka pól, polecam Pick.
Record<K, V> - słownik typowany
Record<K, V> tworzy typ obiektu, w którym klucze mają typ K, a wartości typ V:
1// Słownik: nazwa strefy -> lista dinozaurów
2type ZoneMap = Record<string, Dinosaur[]>;
3
4const parkZones: ZoneMap = {
5 'Zone-A': [{ name: 'Rex', species: 'T-Rex', dangerLevel: 10, enclosureId: 'E-1', dietType: 'carnivore' }],
6 'Zone-B': [{ name: 'Trike', species: 'Triceratops', dangerLevel: 3, enclosureId: 'E-2', dietType: 'herbivore' }]
7};
8
9// Record z ograniczonymi kluczami (union type)
10type DietCount = Record<'herbivore' | 'carnivore' | 'omnivore', number>;
11
12const census: DietCount = {
13 herbivore: 45,
14 carnivore: 12,
15 omnivore: 8
16};Z kluczem string słownik przyjmie dowolną nazwę strefy. Z unią kluczy każdy z nich jest obowiązkowy, więc gdyby w spisie zabrakło omnivore, kompilator zgłosi błąd. To świetny sposób, by nie zapomnieć o żadnej kategorii.
Readonly<T> - niezmienialny typ
Readonly<T> sprawia, że wszystkie właściwości stają się tylko do odczytu:
1type FrozenDinosaur = Readonly<Dinosaur>;
2
3const frozenDNA: FrozenDinosaur = {
4 name: 'Amber Specimen',
5 species: 'Unknown',
6 dangerLevel: 0,
7 enclosureId: 'LAB-1',
8 dietType: 'herbivore'
9};
10
11// frozenDNA.name = 'Changed'; // Błąd! Cannot assign to 'name' because it is a read-only propertyW odróżnieniu od Object.freeze() z lekcji o prototypach ta ochrona działa tylko podczas kompilacji. W działającym JavaScript obiekt pozostaje zwykłym obiektem. Readonly jest też płytki i nie chroni obiektów zagnieżdżonych.
Łączenie typów narzędziowych
Prawdziwa moc pojawia się przy łączeniu kilku utility types. Takie zapisy czytaj od środka na zewnątrz:
1// Formularz tworzenia dinozaura: wszystkie pola wymagane OPRÓCZ enclosureId
2type CreateDinosaurForm = Omit<Dinosaur, 'enclosureId'> & {
3 enclosureId?: string; // opcjonalne przy tworzeniu
4};
5
6// Formularz edycji: tylko wybrane pola, wszystkie opcjonalne
7type EditDinosaurForm = Partial<Pick<Dinosaur, 'name' | 'dangerLevel' | 'enclosureId'>>;
8
9// Niezmienialny podgląd z wybranymi polami
10type DinosaurReadonlyPreview = Readonly<Pick<Dinosaur, 'name' | 'species' | 'dangerLevel'>>;
11
12const readonlyPreview: DinosaurReadonlyPreview = {
13 name: 'Blue',
14 species: 'Velociraptor',
15 dangerLevel: 8
16};
17// readonlyPreview.dangerLevel = 10; // Błąd!EditDinosaurForm najpierw wybiera trzy pola, a potem czyni je opcjonalnymi. W CreateDinosaurForm znak & to intersekcja: łączy typ bez enclosureId z obiektem, w którym to pole jest opcjonalne.
Podsumowanie
Typy narzędziowe TypeScript pozwalają tworzyć warianty istniejących typów bez duplikacji kodu:
| Typ | Działanie |
|---|---|
Partial<T> | Wszystkie pola opcjonalne |
Required<T> | Wszystkie pola wymagane |
Pick<T, K> | Wybierz konkretne pola |
Omit<T, K> | Pomiń konkretne pola |
Record<K, V> | Słownik typowany |
Readonly<T> | Wszystkie pola tylko do odczytu |
To nie cała skrzynka narzędzi: dokumentacja TypeScript opisuje też między innymi ReturnType, Parameters, Exclude i NonNullable. Jak mówi Dr. Wu: "Nie twórz 10 osobnych interfejsów, gdy jeden bazowy typ i utility types załatwią sprawę elegancko i bezpiecznie!" W następnej lekcji zajmiemy się konfiguracją TypeScript i narzędziami wokół kompilatora.
Praktyka
W piaskownicy czeka interfejs Dinosaur z przykładami Partial i Pick. Dopisz wariant z Omit i celowo zrób literówkę w kluczu, żeby zobaczyć różnicę między Pick a Omit.
Kod do tej lekcji: index.ts
1// Typy narzędziowe TypeScript (Utility Types)
2// System zarzadzania dinozaurami w Parku Jurajskim
3
4// Bazowy interfejs dinozaura
5interface Dinosaur {
6 name: string;
7 species: string;
8 dangerLevel: number;
9 enclosureId: string;
10 dietType: "herbivore" | "carnivore" | "omnivore";
11}
12
13// === Partial<T> - wszystkie pola opcjonalne ===
14type DinosaurUpdate = Partial<Dinosaur>;
15
16function updateDinosaur(id: string, changes: DinosaurUpdate): void {
17 console.log("Aktualizuje dinozaura " + id + ":", changes);
18}
19
20updateDinosaur("DINO-001", { dangerLevel: 9 });
21updateDinosaur("DINO-002", { name: "Blue", enclosureId: "E-7" });
22
23// === Pick<T, K> - wybierz wlasciwosci ===
24type DinosaurPreview = Pick<Dinosaur, "name" | "species">;
25
26const preview: DinosaurPreview = {
27 name: "Blue",
28 species: "Velociraptor",
29};
30console.log("\nPodglad:", preview);
31
32// === Omit<T, K> - pomin wlasciwosci ===
33type DinosaurWithoutEnclosure = Omit<Dinosaur, "enclosureId">;
34
35const newDiscovery: DinosaurWithoutEnclosure = {
36 name: "Nowy gatunek",
37 species: "Unknownus rex",
38 dangerLevel: 5,
39 dietType: "carnivore",
40};
41console.log("\nNowe odkrycie:", newDiscovery);
42
43// === Record<K, V> - slownik typowany ===
44type DietCount = Record<"herbivore" | "carnivore" | "omnivore", number>;
45
46const census: DietCount = {
47 herbivore: 45,
48 carnivore: 12,
49 omnivore: 8,
50};
51console.log("\nSpis:", census);
52
53// === Readonly<T> - niezmienialny typ ===
54type FrozenDinosaur = Readonly<Dinosaur>;
55
56const frozen: FrozenDinosaur = {
57 name: "Amber Specimen",
58 species: "Unknown",
59 dangerLevel: 0,
60 enclosureId: "LAB-1",
61 dietType: "herbivore",
62};
63console.log("\nZamrozony:", frozen);
64// frozen.name = "Changed"; // Blad! Read-only
65
66// === Laczenie utility types ===
67type EditForm = Partial<Pick<Dinosaur, "name" | "dangerLevel" | "enclosureId">>;
68
69const edit: EditForm = { dangerLevel: 7 };
70console.log("\nFormularz edycji:", edit);Pamiętaj: typ bazowy to oryginalna próbka DNA, a typy narzędziowe hodują z niej odmiany, więc poprawka w próbce trafia do wszystkich linii naraz.
Widzisz błąd w tej lekcji?
Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. Który utility type w TypeScript sprawia, że WSZYSTKIE właściwości obiektu stają się opcjonalne?
2. Co robi typ Omit<T, K> w TypeScript?
To 2 z 3 pytań do tej lekcji. Pozostałe rozwiążesz w grze.
Zadania praktyczne w grze
- Edytor kodu
Użyj Pick, Omit i Partial na interfejsie Dinosaur do stworzenia wariantów typu.
- Układanie w poziomie
Ułóż elementy użycia utility type Pick:
- Układanie w pionie
Uporządkuj koncepty TypeScript od najprostszych do najbardziej złożonych: