Kurs JavaScript i TypeScript · Moduł 6: Podstawy TypeScript

Typy narzędziowe TypeScript (Utility Types)

6 min czytania
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 property

W 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:

TypDział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. 1. Który utility type w TypeScript sprawia, że WSZYSTKIE właściwości obiektu stają się opcjonalne?

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

Przydatne artykuły