JavaScript and TypeScript course Β· Module 7: Advanced TypeScript
Mapped Types
In this lesson12
"Our park is an incredibly complex organism that constantly evolves. Today we need a registry of all threats, tomorrow we'll need a list of essential supplies, and the day after - a full inventory of dinosaurs along with their statistics," explains John Hammond, founder and director of Jurassic Park. "Every time we must process the same basic data, but in different configurations and formats."
In the world of programming, we often face a similar challenge - transforming existing data types into new forms that better fit specific requirements. TypeScript offers an elegant solution to this problem in the form of mapped types, which allow transforming existing types in a systematic and type-safe manner.
What are mapped types?
Mapped types in TypeScript allow creating new types based on existing ones by transforming the properties of the original type according to a specified pattern. This works similarly to the map() method for arrays in JavaScript, but at the type level - instead of transforming values in an array, we transform properties in a type.
The basic syntax of a mapped type looks as follows:
1type MappedType<T> = {
2 [K in keyof T]: TransformedType
3}In this schema:
Tis the source typeK in keyof Titerates through all keys of typeTTransformedTypeis the new type that will be assigned to each property
Simple example: Readonly
One of the simplest examples of a mapped type is Readonly<T>, which creates a new type where all properties of the source type T are read-only:
1type Readonly<T> = {
2 readonly [K in keyof T]: T[K];
3};Let's apply this to our Jurassic Park example:
1// Dinosaur type definition
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// Creating an immutable dinosaur type
13type ReadonlyDinosaur = Readonly<Dinosaur>;
14
15// Usage
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// This won't compile - properties are read-only
27// trex.name = "Rex"; // Error: Cannot assign to 'name' because it is a read-only propertyChanging property modifiers
Mapped types allow not only transforming property types, but also adding, removing, or modifying modifiers such as readonly or ? (optionality).
Adding modifiers
1// Creates a type where all properties are optional
2type Partial<T> = {
3 [K in keyof T]?: T[K];
4};
5
6// Usage
7type PartialDinosaur = Partial<Dinosaur>;
8
9// Object can contain only some properties
10const newDino: PartialDinosaur = {
11 species: "Velociraptor",
12 diet: "carnivore"
13};Removing modifiers
We can also remove modifiers using the - sign:
1// Creates a type where all properties are required
2type Required<T> = {
3 [K in keyof T]-?: T[K];
4};
5
6// Suppose we have a type with optional properties
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// We create a type where all properties are required
18type RequiredDinosaur = Required<OptionalDinosaur>;
19
20// This won't compile - missing required properties
21// const incompleteDino: RequiredDinosaur = {
22// id: "VEL-02",
23// name: "Blue",
24// species: "Velociraptor"
25// };Transforming property types
Besides modifying property modifiers, we can also transform the types of the properties themselves:
1// Transforms all properties to strings
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", // Must be string, not number
11 weight: "7500", // Must be string, not number
12 diet: "carnivore",
13 enclosure: "Paddock 9"
14};Conditional mapped types
We can combine mapped types with conditional types to selectively transform properties depending on their type:
1// Transforms only numeric properties to strings
2type StringifyNumericProps<T> = {
3 [K in keyof T]: T[K] extends number ? string : T[K];
4};
5
6// Usage
7type DinosaurWithStringifiedNumbers = StringifyNumericProps<Dinosaur>;
8
9const mixedDino: DinosaurWithStringifiedNumbers = {
10 id: "TREX-01",
11 name: "Rexy",
12 species: "Tyrannosaurus Rex",
13 age: "7", // Must be string (originally number)
14 weight: "7500", // Must be string (originally number)
15 diet: "carnivore", // Still a literal union
16 enclosure: "Paddock 9"
17};Practical applications of mapped types
Creating a configuration type based on an existing type
1// Dinosaur parameter type definitions
2interface DinosaurParameters {
3 maxSpeed: number; // km/h
4 length: number; // meters
5 height: number; // meters
6 intelligence: number; // scale 1-10
7 aggressionLevel: number; // scale 1-10
8 socialBehavior: "solitary" | "pair" | "pack";
9}
10
11// We create a configuration type where all parameters are modifiable
12type DinosaurParametersConfig = {
13 [K in keyof DinosaurParameters]: {
14 min: number;
15 max: number;
16 default: DinosaurParameters[K];
17 description: string;
18 };
19};
20
21// Usage
22const velociraptorConfig: DinosaurParametersConfig = {
23 maxSpeed: {
24 min: 30,
25 max: 70,
26 default: 50,
27 description: "Maximum running speed in km/h"
28 },
29 length: {
30 min: 1.5,
31 max: 2.5,
32 default: 2.0,
33 description: "Body length in meters"
34 },
35 height: {
36 min: 0.5,
37 max: 1.0,
38 default: 0.8,
39 description: "Height in meters"
40 },
41 intelligence: {
42 min: 1,
43 max: 10,
44 default: 9,
45 description: "Intelligence level on a scale of 1-10"
46 },
47 aggressionLevel: {
48 min: 1,
49 max: 10,
50 default: 8,
51 description: "Aggression level on a scale of 1-10"
52 },
53 socialBehavior: {
54 min: 0,
55 max: 2,
56 default: "pack",
57 description: "Social behavior: solitary, pair, or pack"
58 }
59};Creating forms based on a data model
1// Data model definition
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// Form type with validation
13type DinosaurFormValidation = {
14 [K in keyof DinosaurRecord]: {
15 required: boolean;
16 validator?: (value: DinosaurRecord[K]) => boolean;
17 errorMessage?: string;
18 };
19};
20
21// Implementation
22const dinoFormValidation: DinosaurFormValidation = {
23 id: {
24 required: true,
25 validator: (id) => /^DINO-\d{3}$/.test(id),
26 errorMessage: "ID must be in DINO-XXX format, where X is a digit"
27 },
28 name: {
29 required: true,
30 validator: (name) => name.length >= 2 && name.length <= 50,
31 errorMessage: "Name must be between 2 and 50 characters"
32 },
33 species: {
34 required: true
35 },
36 age: {
37 required: true,
38 validator: (age) => age > 0 && age < 30,
39 errorMessage: "Age must be between 0 and 30 years"
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: "Last checkup date cannot be in the future"
51 }
52};Tracking changes in objects
1// Type identifying which fields have changed
2type Changed<T> = {
3 [K in keyof T]: boolean;
4};
5
6// Function tracking changes in an object
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// Usage example
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, // Changed
33 weight: 8000, // Changed
34 diet: "carnivore",
35 enclosure: "Paddock 10" // Changed
36};
37
38const changes = trackChanges(originalDino, updatedDino);
39console.log(changes);
40// Result: { id: false, name: false, species: false, age: true, weight: true, diet: false, enclosure: true }Advanced mapped type techniques
Filtering keys based on value types
We can use mapped types together with conditional types to filter object keys based on their value types:
1// Selects keys whose values are of a specific type
2type FilterKeys<T, U> = {
3 [K in keyof T]: T[K] extends U ? K : never;
4}[keyof T];
5
6// Selects a subset of an object based on value type
7type SubType<T, U> = {
8 [K in FilterKeys<T, U>]: T[K];
9};
10
11// Example usage: selecting only numeric properties
12type NumericDinosaurProps = SubType<Dinosaur, number>;
13// Result: { age: number; weight: number; }
14
15// Usage
16const dinoStats: NumericDinosaurProps = {
17 age: 7,
18 weight: 7500
19};Recursive mapped types
We can also create recursive mapped types that transform nested structures:
1// Recursively makes everything read-only
2type DeepReadonly<T> = {
3 readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
4};
5
6// Nested data structure
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// Creating a deeply immutable version
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// Cannot modify any property, even nested ones
59// parkSystem.status = "offline"; // Error
60// parkSystem.subsystems.security.cameras = 50; // Error
61// parkSystem.subsystems.security.fences[0].powered = false; // ErrorAdvanced example: Park resource management system
Below is an advanced example using mapped types to create a flexible resource management system for Jurassic Park:
1// --- Base type definitions ---
2
3// Base interface for all park resources
4interface ParkResource {
5 id: string;
6 name: string;
7 location: string;
8 status: "operational" | "maintenance" | "offline";
9 lastUpdated: Date;
10}
11
12// Different resource types
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; // in 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// Union of all resource types
42type Resource = Dinosaur | Vehicle | Facility | Employee;
43
44// --- Mapped types for resource management ---
45
46// Type for storing resource references
47type ResourceReferences = {
48 [K in Resource["type"]]: string[]; // Array of IDs for each type
49};
50
51// Type for registering management processes for each resource type
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// Type for audit reports
62type AuditReport<T extends ParkResource> = {
63 resource: T;
64 issues: string[];
65 recommendations: string[];
66 complianceScore: number;
67 auditDate: Date;
68};
69
70// Type for resource access permissions
71type ResourceAccessPermissions = {
72 [K in Resource["type"]]: {
73 read: string[]; // Roles that can read
74 write: string[]; // Roles that can modify
75 delete: string[]; // Roles that can delete
76 };
77};
78
79// Type for resource alert statuses
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// Type for resource statistics
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// Type for form metadata
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[]; // For select fields
110 validation?: RegExp; // For text validation
111 min?: number; // For number fields
112 max?: number; // For number fields
113 helpText?: string; // Help text
114 };
115 };
116};
117
118// --- Example implementation ---
119
120// Example form metadata implementation
121const formMetadata: ResourceFormMetadata = {
122 dinosaur: {
123 id: { label: "ID", required: true, type: "text", validation: /^DINO-\d{3}$/, helpText: "Format: DINO-XXX" },
124 name: { label: "Name", required: true, type: "text" },
125 type: { label: "Type", required: true, type: "text" },
126 location: { label: "Location", required: true, type: "text" },
127 status: {
128 label: "Status",
129 required: true,
130 type: "select",
131 options: ["operational", "maintenance", "offline"]
132 },
133 lastUpdated: { label: "Last updated", required: true, type: "date" },
134 species: { label: "Species", required: true, type: "text" },
135 diet: {
136 label: "Diet",
137 required: true,
138 type: "select",
139 options: ["carnivore", "herbivore", "omnivore"]
140 },
141 dangerLevel: {
142 label: "Danger level",
143 required: true,
144 type: "select",
145 options: ["1", "2", "3", "4", "5"],
146 helpText: "1 - Gentle, 5 - Extremely dangerous"
147 }
148 },
149 vehicle: {
150 id: { label: "ID", required: true, type: "text", validation: /^VEH-\d{3}$/, helpText: "Format: VEH-XXX" },
151 name: { label: "Name", required: true, type: "text" },
152 type: { label: "Type", required: true, type: "text" },
153 location: { label: "Location", required: true, type: "text" },
154 status: {
155 label: "Status",
156 required: true,
157 type: "select",
158 options: ["operational", "maintenance", "offline"]
159 },
160 lastUpdated: { label: "Last updated", required: true, type: "date" },
161 model: { label: "Model", required: true, type: "text" },
162 capacity: { label: "Capacity", required: true, type: "number", min: 1, max: 20 },
163 fuelLevel: { label: "Fuel level", required: true, type: "number", min: 0, max: 100, helpText: "In percent" }
164 },
165 facility: {
166 id: { label: "ID", required: true, type: "text", validation: /^FAC-\d{3}$/, helpText: "Format: FAC-XXX" },
167 name: { label: "Name", required: true, type: "text" },
168 type: { label: "Type", required: true, type: "text" },
169 location: { label: "Location", required: true, type: "text" },
170 status: {
171 label: "Status",
172 required: true,
173 type: "select",
174 options: ["operational", "maintenance", "offline"]
175 },
176 lastUpdated: { label: "Last updated", required: true, type: "date" },
177 purpose: {
178 label: "Purpose",
179 required: true,
180 type: "select",
181 options: ["visitor", "research", "maintenance", "security"]
182 },
183 size: { label: "Size", required: true, type: "number", min: 10, helpText: "In square meters" },
184 capacity: { label: "Capacity", 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: "Full name", required: true, type: "text" },
189 type: { label: "Type", required: true, type: "text" },
190 location: { label: "Work station", required: true, type: "text" },
191 status: {
192 label: "Status",
193 required: true,
194 type: "select",
195 options: ["operational", "maintenance", "offline"]
196 },
197 lastUpdated: { label: "Last data update", required: true, type: "date" },
198 role: { label: "Position", required: true, type: "text" },
199 department: {
200 label: "Department",
201 required: true,
202 type: "select",
203 options: ["management", "scientific", "technical", "security", "visitor"]
204 },
205 clearanceLevel: {
206 label: "Clearance level",
207 required: true,
208 type: "select",
209 options: ["1", "2", "3", "4", "5"],
210 helpText: "1 - Basic, 5 - Full access"
211 }
212 }
213};
214
215// Example access permissions
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// Example statistics
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// Example alerts
272const alertStatuses: ResourceAlertStatuses = {
273 dinosaur: {
274 "DINO-001": [
275 {
276 level: "warning",
277 message: "Elevated aggression level over the last 24 hours",
278 timestamp: new Date(),
279 acknowledged: false
280 }
281 ],
282 "DINO-005": [
283 {
284 level: "critical",
285 message: "Dinosaur outside designated enclosure!",
286 timestamp: new Date(),
287 acknowledged: true
288 }
289 ]
290 },
291 facility: {
292 "FAC-003": [
293 {
294 level: "warning",
295 message: "Cooling system failure detected",
296 timestamp: new Date(),
297 acknowledged: false
298 }
299 ]
300 }
301};
302
303// Report generator using mapped types
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 // In a real implementation, data would be fetched from a database
314 return {
315 type: resourceType,
316 stats: resourceStats[resourceType],
317 alerts: alertStatuses[resourceType],
318 // Add resource list only if detailed is true
319 ...(detailed ? { resources: [] as any } : {})
320 };
321}
322
323// Usage example
324const dinosaurReport = generateResourceReport("dinosaur", true);
325console.log(`Dinosaur report:
326- Total count: ${dinosaurReport.stats.total}
327- Operational: ${dinosaurReport.stats.operational}
328- Under maintenance: ${dinosaurReport.stats.maintenance}
329- Offline: ${dinosaurReport.stats.offline}
330- Utilization rate: ${dinosaurReport.stats.utilizationRate}%
331`);
332
333// Display alerts
334if (dinosaurReport.alerts) {
335 console.log("Active alerts:");
336 Object.entries(dinosaurReport.alerts).forEach(([dinoId, alerts]) => {
337 alerts.forEach(alert => {
338 console.log(`- [${alert.level.toUpperCase()}] ${dinoId}: ${alert.message}`);
339 });
340 });
341}Built-in mapped types in TypeScript
TypeScript has several built-in mapped types that are very useful in everyday work:
Partial<T>- Creates a type where all properties ofTare optional1type Partial<T> = { [P in keyof T]?: T[P]; };Required<T>- Creates a type where all properties ofTare required1type Required<T> = { [P in keyof T]-?: T[P]; };Readonly<T>- Creates a type where all properties ofTare read-only1type Readonly<T> = { readonly [P in keyof T]: T[P]; };Record<K, T>- Creates a type with keysKand values of typeT1type Record<K extends keyof any, T> = { [P in K]: T; };Pick<T, K>- Selects a subset of propertiesKfrom typeT1type Pick<T, K extends keyof T> = { [P in K]: T[P]; };Omit<T, K>- Creates a type with all properties ofTexcept those listed inK1type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;Extract<T, U>- Extracts fromTthose types that are assignable toU1type Extract<T, U> = T extends U ? T : never;Exclude<T, U>- Excludes fromTthose types that are assignable toU1type Exclude<T, U> = T extends U ? never : T;NonNullable<T>- ExcludesnullandundefinedfromT1type NonNullable<T> = T extends null | undefined ? never : T;Parameters<T>- Extracts parameter types from a function type1type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;ReturnType<T>- Extracts the return type from a function type1type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
These built-in mapped types are extremely useful and allow for more concise and elegant solutions to many problems.
When to use mapped types?
Mapped types are especially useful in the following scenarios:
Transforming existing types - when you need to create a variant of an existing type (e.g.,
Readonly<T>,Partial<T>)Building forms and validations - when creating dynamic forms based on a data model
Configurations and metadata - when you need to create a configuration or metadata structure for each property of a type
API handling - when you need to transform types to a format required by an API
Generating utility types - when creating utility types that operate on other types
Template literal types
TypeScript offers yet another advanced type mechanism - template literal types. They allow creating new string types through interpolation, similar to template literals in JavaScript, but at the type level.
1// Basic template literal types
2type DinoEvent = `on${string}`;
3
4// Only strings starting with "on" are accepted
5const event1: DinoEvent = "onClick"; // OK
6const event2: DinoEvent = "onEscape"; // OK
7// const event3: DinoEvent = "escape"; // Error!
8
9// Combining with union types
10type Species = "TRex" | "Raptor" | "Triceratops";
11type DinoAction = "feed" | "track" | "relocate";
12
13// Generates: "feedTRex" | "feedRaptor" | ... (all combinations)
14type DinoCommand = `${DinoAction}${Species}`;Template literal types are especially useful for creating precise types for event names, configuration keys, or API paths. They work on the principle of string interpolation, but at the type system level - the TypeScript compiler generates all possible combinations.
Summary
Mapped types are a powerful TypeScript mechanism that allows systematic type transformation. Just as in Jurassic Park, where the same dinosaur data can be processed in different ways depending on needs, mapped types allow us to create new types based on existing ones, adapting them to specific requirements.
Thanks to mapped types, we can create more flexible, type-safe APIs, forms, validations, and many other data structures. This mechanism is especially powerful when combined with other advanced TypeScript features, such as conditional types or type inference.
As you gain more experience with TypeScript, mapped types will become an indispensable tool in your arsenal, allowing you to create more expressive, safe, and elegant solutions.
Code for this lesson: index.ts
1// Mapped types - Jurassic Park
2console.log("=== MAPPED TYPES ===\n");
3
4// ===========================================
5// 1. Built-in mapped types
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> - all fields read-only
18type FrozenDino = Readonly<DinosaurConfig>;
19
20// Partial<T> - all fields optional
21type DinoUpdate = Partial<DinosaurConfig>;
22
23// Pick<T, K> - picking a subset of fields
24type DinoSummary = Pick<DinosaurConfig, "name" | "species" | "dangerLevel">;
25
26// Omit<T, K> - leaving fields out
27type DinoPublic = Omit<DinosaurConfig, "isContained" | "dangerLevel">;
28
29// Record<K, T> - mapping keys to a type
30type DinoRegistry = Record<string, DinosaurConfig>;
31
32// ===========================================
33// 2. Custom mapped types
34// ===========================================
35
36// All fields as nullable
37type Nullable<T> = {
38 [K in keyof T]: T[K] | null;
39};
40
41// All fields as optional and nullable
42type OptionalNullable<T> = {
43 [K in keyof T]?: T[K] | null;
44};
45
46// Numeric fields turned into strings (formatting)
47type Stringify<T> = {
48 [K in keyof T]: T[K] extends number ? string : T[K];
49};
50
51// Only the fields with a numeric type
52type NumericFields<T> = {
53 [K in keyof T as T[K] extends number ? K : never]: T[K];
54};
55
56// Adding a 'get' prefix to the field names
57type Getters<T> = {
58 [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
59};
60
61// ===========================================
62// 3. Practical uses
63// ===========================================
64
65// A form with validation - each field has an error
66type FormErrors<T> = {
67 [K in keyof T]?: string;
68};
69
70// A loading state for each field
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 - a partial update
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("After the update:", 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("Summary:", 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 (custom) ---");
121const maybeDino: Nullable<DinoSummary> = {
122 name: "Unknown",
123 species: null,
124 dangerLevel: null
125};
126console.log("Partial data:", JSON.stringify(maybeDino));
127
128// Stringify
129console.log("\n--- Stringify (custom) ---");
130const formatted: Stringify<DinoSummary> = {
131 name: "Rexy",
132 species: "T-Rex",
133 dangerLevel: "10/10" // number -> string
134};
135console.log("Formatted:", JSON.stringify(formatted));
136
137// FormErrors
138console.log("\n--- FormErrors ---");
139type DinoForm = Pick<DinosaurConfig, "name" | "weight" | "species">;
140const errors: FormErrors<DinoForm> = {
141 name: "Name is required",
142 weight: "Weight must be positive"
143};
144Object.entries(errors).forEach(([field, error]) => {
145 console.log(` ${field}: ${error}`);
146});
147
148console.log("\nMapped types = transforming types like data!");Spotted a mistake in this lesson?
Check yourself
Answer the questions from this lesson. Pick an answer to see right away whether it is correct.
1. What are template literal types in TypeScript?
Hands-on tasks in the game
- Code editor
Create custom mapped types for the park permissions system
- Click in order
Arrange the elements of the Readonly mapped type:
- Click in order
Arrange the elements of a type based on keyof and typeof:
- Vertical ordering
Order the steps for creating a generic utility type: