Kurs JavaScript i TypeScript · Moduł 7: Zaawansowany TypeScript

Mapped types (Typy mapowane)

17 min czytania
W tej lekcji12

"Nasz park to niezwykle złożony organizm, który stale ewoluuje. Dzisiaj potrzebujemy rejestru wszystkich zagrożeń, jutro będziemy potrzebować listy niezbędnych zapasów, a pojutrze - pełnego spisu dinozaurów wraz z ich statystykami," wyjaśnia John Hammond, założyciel i dyrektor Parku Jurajskiego. "Za każdym razem musimy przetwarzać te same podstawowe dane, ale w różnych konfiguracjach i formatach."

W świecie programowania często stajemy przed podobnym wyzwaniem - przetwarzaniem istniejących typów danych w nowe formy, które lepiej pasują do konkretnych wymagań. TypeScript oferuje eleganckie rozwiązanie tego problemu w postaci typów mapowanych (mapped types), które pozwalają na transformację istniejących typów w sposób systematyczny i typowo bezpieczny.

Czym są typy mapowane?

Typy mapowane w TypeScript pozwalają tworzyć nowe typy na podstawie istniejących poprzez przekształcenie właściwości oryginalnego typu według określonego wzorca. Działa to podobnie do metody map() dla tablic w JavaScript, ale na poziomie typów - zamiast transformować wartości w tablicy, transformujemy właściwości w typie.

Podstawowa składnia typu mapowanego wygląda następująco:

1type MappedType<T> = {
2  [K in keyof T]: TransformedType
3}

W tym schemacie:

  • T to typ źródłowy
  • K in keyof T iteruje przez wszystkie klucze typu T
  • TransformedType to nowy typ, który zostanie przypisany do każdej właściwości

Prosty przykład: Readonly

Jednym z najprostszych przykładów typu mapowanego jest Readonly<T>, który tworzy nowy typ, w którym wszystkie właściwości źródłowego typu T są tylko do odczytu:

1type Readonly<T> = {
2  readonly [K in keyof T]: T[K];
3};

Zastosujmy to do naszego przykładu z Parkiem Jurajskim:

1// Definicja typu dla dinozaura
2interface Dinosaur {
3  id: string;
4  name: string;
5  species: string;
6  age: number;
7  weight: number;
8  diet: "carnivore" | "herbivore" | "omnivore";
9  enclosure: string;
10}
11
12// Tworzenie niemodyfikowalnego typu dinozaura
13type ReadonlyDinosaur = Readonly<Dinosaur>;
14
15// Użycie
16const trex: ReadonlyDinosaur = {
17  id: "TREX-01",
18  name: "Rexy",
19  species: "Tyrannosaurus Rex",
20  age: 7,
21  weight: 7500,
22  diet: "carnivore",
23  enclosure: "Paddock 9"
24};
25
26// To nie skompiluje się - właściwości są tylko do odczytu
27// trex.name = "Rex"; // Error: Cannot assign to 'name' because it is a read-only property

Zmiana modyfikatorów właściwości

Typy mapowane pozwalają nie tylko na transformację typów właściwości, ale także na dodawanie, usuwanie lub modyfikowanie modyfikatorów, takich jak readonly czy ? (opcjonalność).

Dodawanie modyfikatorów

1// Tworzy typ, gdzie wszystkie właściwości są opcjonalne
2type Partial<T> = {
3  [K in keyof T]?: T[K];
4};
5
6// Użycie
7type PartialDinosaur = Partial<Dinosaur>;
8
9// Obiekt może zawierać tylko część właściwości
10const newDino: PartialDinosaur = {
11  species: "Velociraptor",
12  diet: "carnivore"
13};

Usuwanie modyfikatorów

Możemy również usunąć modyfikatory, używając znaku -:

1// Tworzy typ, gdzie wszystkie właściwości są wymagane
2type Required<T> = {
3  [K in keyof T]-?: T[K];
4};
5
6// Załóżmy, że mamy typ z opcjonalnymi właściwościami
7interface OptionalDinosaur {
8  id: string;
9  name: string;
10  species: string;
11  age?: number;
12  weight?: number;
13  diet?: "carnivore" | "herbivore" | "omnivore";
14  enclosure?: string;
15}
16
17// Tworzymy typ, gdzie wszystkie właściwości są wymagane
18type RequiredDinosaur = Required<OptionalDinosaur>;
19
20// To nie skompiluje się - brakuje wymaganych właściwości
21// const incompleteDino: RequiredDinosaur = {
22//   id: "VEL-02",
23//   name: "Blue",
24//   species: "Velociraptor"
25// };

Transformacja typów właściwości

Oprócz modyfikowania modyfikatorów właściwości, możemy również transformować typy samych właściwości:

1// Transformuje wszystkie właściwości na stringi
2type StringifiedDinosaur = {
3  [K in keyof Dinosaur]: string;
4};
5
6const strDino: StringifiedDinosaur = {
7  id: "TREX-01",
8  name: "Rexy",
9  species: "Tyrannosaurus Rex",
10  age: "7",           // Musi być string, nie number
11  weight: "7500",     // Musi być string, nie number
12  diet: "carnivore",
13  enclosure: "Paddock 9"
14};

Warunkowe typy mapowane

Możemy łączyć typy mapowane z typami warunkowymi, aby selektywnie transformować właściwości w zależności od ich typu:

1// Transformuje tylko numeryczne właściwości na stringi
2type StringifyNumericProps<T> = {
3  [K in keyof T]: T[K] extends number ? string : T[K];
4};
5
6// Użycie
7type DinosaurWithStringifiedNumbers = StringifyNumericProps<Dinosaur>;
8
9const mixedDino: DinosaurWithStringifiedNumbers = {
10  id: "TREX-01",
11  name: "Rexy",
12  species: "Tyrannosaurus Rex",
13  age: "7",           // Musi być string (oryginalnie number)
14  weight: "7500",     // Musi być string (oryginalnie number)
15  diet: "carnivore",  // Wciąż unia literałów
16  enclosure: "Paddock 9"
17};

Praktyczne zastosowania typów mapowanych

Tworzenie typu konfiguracji na podstawie istniejącego typu

1// Definicja typów dla parametrów dinozaura
2interface DinosaurParameters {
3  maxSpeed: number;        // km/h
4  length: number;          // metry
5  height: number;          // metry
6  intelligence: number;    // skala 1-10
7  aggressionLevel: number; // skala 1-10
8  socialBehavior: "solitary" | "pair" | "pack";
9}
10
11// Tworzymy typ konfiguracji, gdzie wszystkie parametry są modyfikowalne
12type DinosaurParametersConfig = {
13  [K in keyof DinosaurParameters]: {
14    min: number;
15    max: number;
16    default: DinosaurParameters[K];
17    description: string;
18  };
19};
20
21// Użycie
22const velociraptorConfig: DinosaurParametersConfig = {
23  maxSpeed: {
24    min: 30,
25    max: 70,
26    default: 50,
27    description: "Maksymalna prędkość biegu w km/h"
28  },
29  length: {
30    min: 1.5,
31    max: 2.5,
32    default: 2.0,
33    description: "Długość ciała w metrach"
34  },
35  height: {
36    min: 0.5,
37    max: 1.0,
38    default: 0.8,
39    description: "Wysokość w metrach"
40  },
41  intelligence: {
42    min: 1,
43    max: 10,
44    default: 9,
45    description: "Poziom inteligencji w skali 1-10"
46  },
47  aggressionLevel: {
48    min: 1,
49    max: 10,
50    default: 8,
51    description: "Poziom agresji w skali 1-10"
52  },
53  socialBehavior: {
54    min: 0,
55    max: 2,
56    default: "pack",
57    description: "Zachowania społeczne: samotnik, para lub stado"
58  }
59};

Tworzenie formularzy na podstawie modelu danych

1// Definicja modelu danych
2interface DinosaurRecord {
3  id: string;
4  name: string;
5  species: string;
6  age: number;
7  diet: "carnivore" | "herbivore" | "omnivore";
8  healthStatus: "healthy" | "sick" | "critical";
9  lastCheckup: Date;
10}
11
12// Typ formularza z walidacją
13type DinosaurFormValidation = {
14  [K in keyof DinosaurRecord]: {
15    required: boolean;
16    validator?: (value: DinosaurRecord[K]) => boolean;
17    errorMessage?: string;
18  };
19};
20
21// Implementacja
22const dinoFormValidation: DinosaurFormValidation = {
23  id: {
24    required: true,
25    validator: (id) => /^DINO-\d{3}$/.test(id),
26    errorMessage: "ID musi być w formacie DINO-XXX, gdzie X to cyfra"
27  },
28  name: {
29    required: true,
30    validator: (name) => name.length >= 2 && name.length <= 50,
31    errorMessage: "Imię musi mieć od 2 do 50 znaków"
32  },
33  species: {
34    required: true
35  },
36  age: {
37    required: true,
38    validator: (age) => age > 0 && age < 30,
39    errorMessage: "Wiek musi być między 0 a 30 lat"
40  },
41  diet: {
42    required: true
43  },
44  healthStatus: {
45    required: true
46  },
47  lastCheckup: {
48    required: true,
49    validator: (date) => date <= new Date(),
50    errorMessage: "Data ostatniego badania nie może być w przyszłości"
51  }
52};

Śledzenie zmian w obiektach

1// Typ identyfikujący, które pola zostały zmienione
2type Changed<T> = {
3  [K in keyof T]: boolean;
4};
5
6// Funkcja śledząca zmiany w obiekcie
7function trackChanges<T>(original: T, updated: T): Changed<T> {
8  const result = {} as Changed<T>;
9
10  for (const key in original) {
11    result[key] = original[key] !== updated[key];
12  }
13
14  return result;
15}
16
17// Przykład użycia
18const originalDino: Dinosaur = {
19  id: "TREX-01",
20  name: "Rexy",
21  species: "Tyrannosaurus Rex",
22  age: 7,
23  weight: 7500,
24  diet: "carnivore",
25  enclosure: "Paddock 9"
26};
27
28const updatedDino: Dinosaur = {
29  id: "TREX-01",
30  name: "Rexy",
31  species: "Tyrannosaurus Rex",
32  age: 8,          // Zmieniono
33  weight: 8000,    // Zmieniono
34  diet: "carnivore",
35  enclosure: "Paddock 10" // Zmieniono
36};
37
38const changes = trackChanges(originalDino, updatedDino);
39console.log(changes);
40// Wynik: { id: false, name: false, species: false, age: true, weight: true, diet: false, enclosure: true }

Zaawansowane techniki typów mapowanych

Filtrowanie kluczy na podstawie typów wartości

Możemy użyć typów mapowanych razem z typami warunkowymi, aby filtrować klucze obiektu na podstawie typów ich wartości:

1// Wybiera klucze, których wartości są określonego typu
2type FilterKeys<T, U> = {
3  [K in keyof T]: T[K] extends U ? K : never;
4}[keyof T];
5
6// Wybiera podzbiór obiektu na podstawie typu wartości
7type SubType<T, U> = {
8  [K in FilterKeys<T, U>]: T[K];
9};
10
11// Przykładowe użycie: wybieranie tylko numerycznych właściwości
12type NumericDinosaurProps = SubType<Dinosaur, number>;
13// Wynik: { age: number; weight: number; }
14
15// Użycie
16const dinoStats: NumericDinosaurProps = {
17  age: 7,
18  weight: 7500
19};

Rekursywne typy mapowane

Możemy również tworzyć rekursywne typy mapowane, które transformują zagnieżdżone struktury:

1// Rekursywnie tworzy tylko do odczytu
2type DeepReadonly<T> = {
3  readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
4};
5
6// Zagnieżdżona struktura danych
7interface ParkSystem {
8  name: string;
9  status: "online" | "offline" | "maintenance";
10  subsystems: {
11    security: {
12      cameras: number;
13      fences: {
14        powered: boolean;
15        voltage: number;
16      }[];
17    };
18    power: {
19      mainGenerator: {
20        output: number;
21        fuel: number;
22      };
23      backupGenerator: {
24        output: number;
25        fuel: number;
26      };
27    };
28  };
29}
30
31// Tworzenie głęboko niemutowalnej wersji
32type ReadonlyParkSystem = DeepReadonly<ParkSystem>;
33
34const parkSystem: ReadonlyParkSystem = {
35  name: "Jurassic Park Control System",
36  status: "online",
37  subsystems: {
38    security: {
39      cameras: 48,
40      fences: [
41        { powered: true, voltage: 10000 },
42        { powered: true, voltage: 10000 }
43      ]
44    },
45    power: {
46      mainGenerator: {
47        output: 8000,
48        fuel: 75
49      },
50      backupGenerator: {
51        output: 3000,
52        fuel: 100
53      }
54    }
55  }
56};
57
58// Nie można modyfikować żadnej właściwości, nawet zagnieżdżonej
59// parkSystem.status = "offline"; // Error
60// parkSystem.subsystems.security.cameras = 50; // Error
61// parkSystem.subsystems.security.fences[0].powered = false; // Error

Zaawansowany przykład: System zarządzania zasobami parku

Poniżej znajduje się zaawansowany przykład wykorzystujący typy mapowane do stworzenia elastycznego systemu zarządzania zasobami Parku Jurajskiego:

1// --- Definicje typów bazowych ---
2
3// Bazowy interfejs dla wszystkich zasobów parku
4interface ParkResource {
5  id: string;
6  name: string;
7  location: string;
8  status: "operational" | "maintenance" | "offline";
9  lastUpdated: Date;
10}
11
12// Różne typy zasobów
13interface Dinosaur extends ParkResource {
14  type: "dinosaur";
15  species: string;
16  diet: "carnivore" | "herbivore" | "omnivore";
17  dangerLevel: 1 | 2 | 3 | 4 | 5;
18}
19
20interface Vehicle extends ParkResource {
21  type: "vehicle";
22  model: string;
23  capacity: number;
24  fuelLevel: number;
25}
26
27interface Facility extends ParkResource {
28  type: "facility";
29  purpose: "visitor" | "research" | "maintenance" | "security";
30  size: number; // w m²
31  capacity: number;
32}
33
34interface Employee extends ParkResource {
35  type: "employee";
36  role: string;
37  department: "management" | "scientific" | "technical" | "security" | "visitor";
38  clearanceLevel: 1 | 2 | 3 | 4 | 5;
39}
40
41// Unia wszystkich typów zasobów
42type Resource = Dinosaur | Vehicle | Facility | Employee;
43
44// --- Typy mapowane dla zarządzania zasobami ---
45
46// Typ do przechowywania referencji do zasobów
47type ResourceReferences = {
48  [K in Resource["type"]]: string[]; // Tablica ID dla każdego typu
49};
50
51// Typ do rejestracji procesów zarządzania dla każdego typu zasobu
52type ResourceManagementProcesses = {
53  [K in Resource["type"]]: {
54    create: (data: Omit<Extract<Resource, { type: K }>, "id" | "lastUpdated">) => string;
55    update: (id: string, data: Partial<Extract<Resource, { type: K }>>) => boolean;
56    delete: (id: string) => boolean;
57    audit: (id: string) => AuditReport<Extract<Resource, { type: K }>>;
58  };
59};
60
61// Typ dla raportów audytowych
62type AuditReport<T extends ParkResource> = {
63  resource: T;
64  issues: string[];
65  recommendations: string[];
66  complianceScore: number;
67  auditDate: Date;
68};
69
70// Typ dla uprawnień dostępu do zasobów
71type ResourceAccessPermissions = {
72  [K in Resource["type"]]: {
73    read: string[];   // Role, które mogą czytać
74    write: string[];  // Role, które mogą modyfikować
75    delete: string[]; // Role, które mogą usuwać
76  };
77};
78
79// Typ dla stanów alertów dla zasobów
80type ResourceAlertStatuses = {
81  [K in Resource["type"]]?: {
82    [ID: string]: {
83      level: "info" | "warning" | "critical";
84      message: string;
85      timestamp: Date;
86      acknowledged: boolean;
87    }[];
88  };
89};
90
91// Typ dla statystyk zasobów
92type ResourceStatistics = {
93  [K in Resource["type"]]: {
94    total: number;
95    operational: number;
96    maintenance: number;
97    offline: number;
98    utilizationRate: number; // %
99  };
100};
101
102// Typ dla metadanych formularzy
103type ResourceFormMetadata = {
104  [K in Resource["type"]]: {
105    [Field in keyof Extract<Resource, { type: K }>]: {
106      label: string;
107      required: boolean;
108      type: "text" | "number" | "select" | "date" | "checkbox";
109      options?: string[]; // Dla pól select
110      validation?: RegExp; // Dla walidacji tekstowej
111      min?: number;       // Dla pól liczbowych
112      max?: number;       // Dla pól liczbowych
113      helpText?: string;  // Tekst pomocniczy
114    };
115  };
116};
117
118// --- Implementacja przykładowa ---
119
120// Przykładowa implementacja metadanych formularzy
121const formMetadata: ResourceFormMetadata = {
122  dinosaur: {
123    id: { label: "ID", required: true, type: "text", validation: /^DINO-\d{3}$/, helpText: "Format: DINO-XXX" },
124    name: { label: "Nazwa", required: true, type: "text" },
125    type: { label: "Typ", required: true, type: "text" },
126    location: { label: "Lokalizacja", required: true, type: "text" },
127    status: {
128      label: "Status",
129      required: true,
130      type: "select",
131      options: ["operational", "maintenance", "offline"]
132    },
133    lastUpdated: { label: "Ostatnia aktualizacja", required: true, type: "date" },
134    species: { label: "Gatunek", required: true, type: "text" },
135    diet: {
136      label: "Dieta",
137      required: true,
138      type: "select",
139      options: ["carnivore", "herbivore", "omnivore"]
140    },
141    dangerLevel: {
142      label: "Poziom zagrożenia",
143      required: true,
144      type: "select",
145      options: ["1", "2", "3", "4", "5"],
146      helpText: "1 - Łagodny, 5 - Ekstremalnie niebezpieczny"
147    }
148  },
149  vehicle: {
150    id: { label: "ID", required: true, type: "text", validation: /^VEH-\d{3}$/, helpText: "Format: VEH-XXX" },
151    name: { label: "Nazwa", required: true, type: "text" },
152    type: { label: "Typ", required: true, type: "text" },
153    location: { label: "Lokalizacja", required: true, type: "text" },
154    status: {
155      label: "Status",
156      required: true,
157      type: "select",
158      options: ["operational", "maintenance", "offline"]
159    },
160    lastUpdated: { label: "Ostatnia aktualizacja", required: true, type: "date" },
161    model: { label: "Model", required: true, type: "text" },
162    capacity: { label: "Pojemność", required: true, type: "number", min: 1, max: 20 },
163    fuelLevel: { label: "Poziom paliwa", required: true, type: "number", min: 0, max: 100, helpText: "W procentach" }
164  },
165  facility: {
166    id: { label: "ID", required: true, type: "text", validation: /^FAC-\d{3}$/, helpText: "Format: FAC-XXX" },
167    name: { label: "Nazwa", required: true, type: "text" },
168    type: { label: "Typ", required: true, type: "text" },
169    location: { label: "Lokalizacja", required: true, type: "text" },
170    status: {
171      label: "Status",
172      required: true,
173      type: "select",
174      options: ["operational", "maintenance", "offline"]
175    },
176    lastUpdated: { label: "Ostatnia aktualizacja", required: true, type: "date" },
177    purpose: {
178      label: "Przeznaczenie",
179      required: true,
180      type: "select",
181      options: ["visitor", "research", "maintenance", "security"]
182    },
183    size: { label: "Rozmiar", required: true, type: "number", min: 10, helpText: "W metrach kwadratowych" },
184    capacity: { label: "Pojemność", required: true, type: "number", min: 1 }
185  },
186  employee: {
187    id: { label: "ID", required: true, type: "text", validation: /^EMP-\d{3}$/, helpText: "Format: EMP-XXX" },
188    name: { label: "Imię i nazwisko", required: true, type: "text" },
189    type: { label: "Typ", required: true, type: "text" },
190    location: { label: "Stanowisko pracy", required: true, type: "text" },
191    status: {
192      label: "Status",
193      required: true,
194      type: "select",
195      options: ["operational", "maintenance", "offline"]
196    },
197    lastUpdated: { label: "Ostatnia aktualizacja danych", required: true, type: "date" },
198    role: { label: "Stanowisko", required: true, type: "text" },
199    department: {
200      label: "Dział",
201      required: true,
202      type: "select",
203      options: ["management", "scientific", "technical", "security", "visitor"]
204    },
205    clearanceLevel: {
206      label: "Poziom dostępu",
207      required: true,
208      type: "select",
209      options: ["1", "2", "3", "4", "5"],
210      helpText: "1 - Podstawowy, 5 - Pełny dostęp"
211    }
212  }
213};
214
215// Przykładowe uprawnienia dostępu
216const accessPermissions: ResourceAccessPermissions = {
217  dinosaur: {
218    read: ["management", "scientific", "technical", "security", "visitor"],
219    write: ["management", "scientific"],
220    delete: ["management"]
221  },
222  vehicle: {
223    read: ["management", "technical", "security"],
224    write: ["management", "technical"],
225    delete: ["management"]
226  },
227  facility: {
228    read: ["management", "technical", "security"],
229    write: ["management", "technical"],
230    delete: ["management"]
231  },
232  employee: {
233    read: ["management"],
234    write: ["management"],
235    delete: ["management"]
236  }
237};
238
239// Przykładowe statystyki
240const resourceStats: ResourceStatistics = {
241  dinosaur: {
242    total: 15,
243    operational: 12,
244    maintenance: 2,
245    offline: 1,
246    utilizationRate: 80
247  },
248  vehicle: {
249    total: 25,
250    operational: 20,
251    maintenance: 3,
252    offline: 2,
253    utilizationRate: 75
254  },
255  facility: {
256    total: 12,
257    operational: 10,
258    maintenance: 1,
259    offline: 1,
260    utilizationRate: 85
261  },
262  employee: {
263    total: 120,
264    operational: 115,
265    maintenance: 0,
266    offline: 5,
267    utilizationRate: 90
268  }
269};
270
271// Przykładowe alerty
272const alertStatuses: ResourceAlertStatuses = {
273  dinosaur: {
274    "DINO-001": [
275      {
276        level: "warning",
277        message: "Podwyższony poziom agresji w ciągu ostatnich 24 godzin",
278        timestamp: new Date(),
279        acknowledged: false
280      }
281    ],
282    "DINO-005": [
283      {
284        level: "critical",
285        message: "Dinozaur poza wyznaczoną zagrodą!",
286        timestamp: new Date(),
287        acknowledged: true
288      }
289    ]
290  },
291  facility: {
292    "FAC-003": [
293      {
294        level: "warning",
295        message: "Wykryto awarię systemu chłodzenia",
296        timestamp: new Date(),
297        acknowledged: false
298      }
299    ]
300  }
301};
302
303// Generator raportów wykorzystujący typy mapowane
304function generateResourceReport<T extends Resource["type"]>(
305  resourceType: T,
306  detailed: boolean = false
307): {
308  type: T;
309  stats: ResourceStatistics[T];
310  alerts: ResourceAlertStatuses[T] | undefined;
311  resources?: Extract<Resource, { type: T }>[];
312} {
313  // W rzeczywistej implementacji, dane byłyby pobierane z bazy danych
314  return {
315    type: resourceType,
316    stats: resourceStats[resourceType],
317    alerts: alertStatuses[resourceType],
318    // Dodajemy listę zasobów tylko jeśli detailed jest true
319    ...(detailed ? { resources: [] as any } : {})
320  };
321}
322
323// Przykładowe użycie
324const dinosaurReport = generateResourceReport("dinosaur", true);
325console.log(`Raport dla dinozaurów:
326- Łączna liczba: ${dinosaurReport.stats.total}
327- Operacyjne: ${dinosaurReport.stats.operational}
328- W trakcie konserwacji: ${dinosaurReport.stats.maintenance}
329- Offline: ${dinosaurReport.stats.offline}
330- Współczynnik wykorzystania: ${dinosaurReport.stats.utilizationRate}%
331`);
332
333// Wyświetlenie alertów
334if (dinosaurReport.alerts) {
335  console.log("Aktywne alerty:");
336  Object.entries(dinosaurReport.alerts).forEach(([dinoId, alerts]) => {
337    alerts.forEach(alert => {
338      console.log(`- [${alert.level.toUpperCase()}] ${dinoId}: ${alert.message}`);
339    });
340  });
341}

Wbudowane typy mapowane w TypeScript

TypeScript posiada kilka wbudowanych typów mapowanych, które są bardzo przydatne w codziennej pracy:

  1. Partial<T> - Tworzy typ, w którym wszystkie właściwości T są opcjonalne

    1type Partial<T> = { [P in keyof T]?: T[P]; };
  2. Required<T> - Tworzy typ, w którym wszystkie właściwości T są wymagane

    1type Required<T> = { [P in keyof T]-?: T[P]; };
  3. Readonly<T> - Tworzy typ, w którym wszystkie właściwości T są tylko do odczytu

    1type Readonly<T> = { readonly [P in keyof T]: T[P]; };
  4. Record<K, T> - Tworzy typ z kluczami K i wartościami typu T

    1type Record<K extends keyof any, T> = { [P in K]: T; };
  5. Pick<T, K> - Wybiera podzbiór właściwości K z typu T

    1type Pick<T, K extends keyof T> = { [P in K]: T[P]; };
  6. Omit<T, K> - Tworzy typ z wszystkimi właściwościami T oprócz tych wymienionych w K

    1type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
  7. Extract<T, U> - Wyciąga z T te typy, które można przypisać do U

    1type Extract<T, U> = T extends U ? T : never;
  8. Exclude<T, U> - Wyklucza z T te typy, które można przypisać do U

    1type Exclude<T, U> = T extends U ? never : T;
  9. NonNullable<T> - Wyklucza null i undefined z T

    1type NonNullable<T> = T extends null | undefined ? never : T;
  10. Parameters<T> - Wyciąga typy parametrów z typu funkcji

    1type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;
  11. ReturnType<T> - Wyciąga typ zwracany z typu funkcji

    1type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;

Te wbudowane typy mapowane są niezwykle przydatne i pozwalają na bardziej zwięzłe i eleganckie rozwiązania wielu problemów.

Kiedy używać typów mapowanych?

Typy mapowane są szczególnie przydatne w następujących scenariuszach:

  1. Transformacja istniejących typów - gdy potrzebujesz stworzyć wariant istniejącego typu (np. Readonly<T>, Partial<T>)

  2. Budowanie formularzy i walidacji - gdy tworzysz dynamiczne formularze na podstawie modelu danych

  3. Konfiguracje i metadane - gdy potrzebujesz stworzyć strukturę konfiguracji lub metadanych dla każdej właściwości typu

  4. Obsługa API - gdy potrzebujesz transformować typy do formatu wymaganego przez API

  5. Generowanie typów pomocniczych - gdy tworzysz typy narzędziowe, które działają na innych typach

Template literal types

TypeScript oferuje jeszcze jeden zaawansowany mechanizm typów - template literal types. Pozwalają one tworzyć nowe typy stringowe poprzez interpolację, podobnie jak template literals w JavaScript, ale na poziomie typów.

1// Podstawowe template literal types
2type DinoEvent = `on${string}`;
3
4// Tylko stringi zaczynające się od "on" są akceptowane
5const event1: DinoEvent = "onClick";     // OK
6const event2: DinoEvent = "onEscape";    // OK
7// const event3: DinoEvent = "escape";   // Błąd!
8
9// Łączenie z union types
10type Species = "TRex" | "Raptor" | "Triceratops";
11type DinoAction = "feed" | "track" | "relocate";
12
13// Generuje: "feedTRex" | "feedRaptor" | ... (wszystkie kombinacje)
14type DinoCommand = `${DinoAction}${Species}`;

Template literal types są szczególnie przydatne do tworzenia precyzyjnych typów dla nazw zdarzeń, kluczy konfiguracji czy ścieżek API. Działają one na zasadzie interpolacji stringów, ale na poziomie systemu typów - kompilator TypeScript generuje wszystkie możliwe kombinacje.

Podsumowanie

Typy mapowane są potężnym mechanizmem TypeScript, który pozwala na systematyczną transformację typów. Podobnie jak w Parku Jurajskim, gdzie te same dane o dinozaurach mogą być przetwarzane na różne sposoby w zależności od potrzeb, typy mapowane pozwalają nam tworzyć nowe typy na podstawie istniejących, dostosowując je do konkretnych wymagań.

Dzięki typom mapowanym możemy tworzyć bardziej elastyczne, typowo bezpieczne API, formularze, walidacje i wiele innych struktur danych. Mechanizm ten jest szczególnie potężny w połączeniu z innymi zaawansowanymi funkcjami TypeScript, takimi jak typy warunkowe czy inferencja typów.

W miarę jak będziesz zdobywać więcej doświadczenia w TypeScript, typy mapowane staną się nieodzownym narzędziem w twoim arsenale, pozwalającym na tworzenie bardziej ekspresyjnych, bezpiecznych i eleganckich rozwiązań.

Kod do tej lekcji: index.ts
1// Typy mapowane (Mapped Types) - Park Jurajski
2console.log("=== TYPY MAPOWANE ===\n");
3
4// ===========================================
5// 1. Wbudowane typy mapowane
6// ===========================================
7
8interface DinosaurConfig {
9  name: string;
10  species: string;
11  weight: number;
12  height: number;
13  dangerLevel: number;
14  isContained: boolean;
15}
16
17// Readonly<T> - wszystkie pola tylko do odczytu
18type FrozenDino = Readonly<DinosaurConfig>;
19
20// Partial<T> - wszystkie pola opcjonalne
21type DinoUpdate = Partial<DinosaurConfig>;
22
23// Pick<T, K> - wybranie podzbioru pol
24type DinoSummary = Pick<DinosaurConfig, "name" | "species" | "dangerLevel">;
25
26// Omit<T, K> - pominiecie pol
27type DinoPublic = Omit<DinosaurConfig, "isContained" | "dangerLevel">;
28
29// Record<K, T> - mapowanie kluczy na typ
30type DinoRegistry = Record<string, DinosaurConfig>;
31
32// ===========================================
33// 2. Wlasne typy mapowane
34// ===========================================
35
36// Wszystkie pola jako nullable
37type Nullable<T> = {
38  [K in keyof T]: T[K] | null;
39};
40
41// Wszystkie pola jako opcjonalne i nullable
42type OptionalNullable<T> = {
43  [K in keyof T]?: T[K] | null;
44};
45
46// Pola numeryczne zamienione na string (formatowanie)
47type Stringify<T> = {
48  [K in keyof T]: T[K] extends number ? string : T[K];
49};
50
51// Tylko pola o typie numerycznym
52type NumericFields<T> = {
53  [K in keyof T as T[K] extends number ? K : never]: T[K];
54};
55
56// Dodanie prefixu 'get' do nazw pol
57type Getters<T> = {
58  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
59};
60
61// ===========================================
62// 3. Praktyczne zastosowania
63// ===========================================
64
65// Formularz z walidacja - kazde pole ma blad
66type FormErrors<T> = {
67  [K in keyof T]?: string;
68};
69
70// Stan ladowania dla kazdego pola
71type LoadingState<T> = {
72  [K in keyof T]: {
73    value: T[K];
74    isLoading: boolean;
75    error?: string;
76  };
77};
78
79// ===========================================
80// Test
81// ===========================================
82
83// Readonly
84console.log("--- Readonly ---");
85const frozen: FrozenDino = {
86  name: "Rexy",
87  species: "T-Rex",
88  weight: 8000,
89  height: 5.5,
90  dangerLevel: 10,
91  isContained: true
92};
93console.log("Frozen:", frozen.name);
94// frozen.name = "New"; // Error! - readonly
95
96// Partial - aktualizacja czesciowa
97console.log("\n--- Partial ---");
98function updateDino(current: DinosaurConfig, updates: DinoUpdate): DinosaurConfig {
99  return { ...current, ...updates };
100}
101const updated = updateDino(frozen as DinosaurConfig, { weight: 8200, height: 5.6 });
102console.log("Po aktualizacji:", updated.weight, "kg,", updated.height, "m");
103
104// Pick
105console.log("\n--- Pick ---");
106const summary: DinoSummary = { name: "Rexy", species: "T-Rex", dangerLevel: 10 };
107console.log("Podsumowanie:", JSON.stringify(summary));
108
109// Record
110console.log("\n--- Record ---");
111const registry: DinoRegistry = {
112  "DINO-001": { name: "Rexy", species: "T-Rex", weight: 8000, height: 5.5, dangerLevel: 10, isContained: true },
113  "DINO-002": { name: "Blue", species: "Velociraptor", weight: 15, height: 1.8, dangerLevel: 8, isContained: true },
114};
115Object.entries(registry).forEach(([id, dino]) => {
116  console.log(`${id}: ${dino.name} (${dino.species})`);
117});
118
119// Nullable
120console.log("\n--- Nullable (wlasny) ---");
121const maybeDino: Nullable<DinoSummary> = {
122  name: "Nieznany",
123  species: null,
124  dangerLevel: null
125};
126console.log("Czesciowe dane:", JSON.stringify(maybeDino));
127
128// Stringify
129console.log("\n--- Stringify (wlasny) ---");
130const formatted: Stringify<DinoSummary> = {
131  name: "Rexy",
132  species: "T-Rex",
133  dangerLevel: "10/10" // number -> string
134};
135console.log("Sformatowany:", JSON.stringify(formatted));
136
137// FormErrors
138console.log("\n--- FormErrors ---");
139type DinoForm = Pick<DinosaurConfig, "name" | "weight" | "species">;
140const errors: FormErrors<DinoForm> = {
141  name: "Nazwa jest wymagana",
142  weight: "Waga musi byc dodatnia"
143};
144Object.entries(errors).forEach(([field, error]) => {
145  console.log(`  ${field}: ${error}`);
146});
147
148console.log("\nTypy mapowane = transformacja typow jak danych!");

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. Co to są template literal types w TypeScript?

Zadania praktyczne w grze

  • Edytor kodu

    Stwórz custom mapped types dla systemu uprawnień parku

  • Klikanie w kolejności

    Ułóż elementy mapped type Readonly:

  • Klikanie w kolejności

    Ułóż elementy typu opartego na keyof i typeof:

  • Układanie w pionie

    Uporządkuj etapy tworzenia generycznego utility type:

Przydatne artykuły