"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.
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:
T is the source typeK in keyof T iterates through all keys of type TTransformedType is the new type that will be assigned to each propertyOne 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 propertyMapped types allow not only transforming property types, but also adding, removing, or modifying modifiers such as
readonly or ? (optionality).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};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// };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};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};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};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};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 }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};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; // ErrorBelow 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}TypeScript has several built-in mapped types that are very useful in everyday work:
- Creates a type where all properties of Partial<T>
T are optional1type Partial<T> = { [P in keyof T]?: T[P]; };
- Creates a type where all properties of Required<T>
T are required1type Required<T> = { [P in keyof T]-?: T[P]; };
- Creates a type where all properties of Readonly<T>
T are read-only1type Readonly<T> = { readonly [P in keyof T]: T[P]; };
- Creates a type with keys Record<K, T>
K and values of type T1type Record<K extends keyof any, T> = { [P in K]: T; };
- Selects a subset of properties Pick<T, K>
K from type T1type Pick<T, K extends keyof T> = { [P in K]: T[P]; };
- Creates a type with all properties of Omit<T, K>
T except those listed in K1type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
- Extracts from Extract<T, U>
T those types that are assignable to U1type Extract<T, U> = T extends U ? T : never;
- Excludes from Exclude<T, U>
T those types that are assignable to U1type Exclude<T, U> = T extends U ? never : T;
- Excludes NonNullable<T>
null and undefined from T1type NonNullable<T> = T extends null | undefined ? never : T;
- Extracts parameter types from a function typeParameters<T>
1type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;
- Extracts the return type from a function typeReturnType<T>
1type 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.
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
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.
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.