Cours JavaScript et TypeScript · Module 7: TypeScript avancé
Types mappés
Dans cette leçon12
« Notre parc est un organisme incroyablement complexe qui évolue sans cesse. Aujourd'hui, nous avons besoin d'un registre de toutes les menaces, demain ce sera une liste des fournitures essentielles, et après-demain - un inventaire complet des dinosaures avec leurs statistiques », explique John Hammond, fondateur et directeur de Jurassic Park. « Chaque fois, nous devons traiter les mêmes données de base, mais dans des configurations et des formats différents. »
Dans le monde de la programmation, nous sommes souvent confrontés à un défi similaire - transformer des types de données existants en nouvelles formes mieux adaptées à des exigences précises. TypeScript propose une solution élégante à ce problème sous la forme des types mappés, qui permettent de transformer des types existants de manière systématique et sûre en matière de types.
Que sont les types mappés ?
Les types mappés en TypeScript permettent de créer de nouveaux types à partir de types existants, en transformant les propriétés du type d'origine selon un patron défini. Cela fonctionne de manière similaire à la méthode map() pour les tableaux en JavaScript, mais au niveau des types - au lieu de transformer des valeurs dans un tableau, nous transformons des propriétés dans un type.
La syntaxe de base d'un type mappé se présente comme suit :
1type MappedType<T> = {
2 [K in keyof T]: TransformedType
3}Dans ce schéma :
Test le type sourceK in keyof Tparcourt toutes les clés du typeTTransformedTypeest le nouveau type qui sera assigné à chaque propriété
Exemple simple : Readonly
L'un des exemples les plus simples de type mappé est Readonly<T>, qui crée un nouveau type où toutes les propriétés du type source T sont en lecture seule :
1type Readonly<T> = {
2 readonly [K in keyof T]: T[K];
3};Appliquons cela à notre exemple de Jurassic Park :
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 propertyModifier les modificateurs de propriété
Les types mappés permettent non seulement de transformer les types des propriétés, mais aussi d'ajouter, de supprimer ou de modifier des modificateurs tels que readonly ou ? (optionnalité).
Ajouter des modificateurs
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};Supprimer des modificateurs
On peut aussi supprimer des modificateurs à l'aide du signe - :
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// };Transformer les types des propriétés
Outre la modification des modificateurs de propriété, on peut aussi transformer les types des propriétés elles-mêmes :
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};Types mappés conditionnels
On peut combiner les types mappés avec les types conditionnels pour transformer sélectivement les propriétés selon leur 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};Applications pratiques des types mappés
Créer un type de configuration à partir d'un type existant
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};Créer des formulaires à partir d'un modèle de données
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};Suivre les changements dans les objets
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 }Techniques avancées des types mappés
Filtrer les clés selon les types de valeurs
On peut utiliser les types mappés conjointement avec les types conditionnels pour filtrer les clés d'un objet selon le type de leurs valeurs :
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};Types mappés récursifs
On peut aussi créer des types mappés récursifs qui transforment des structures imbriquées :
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; // ErrorExemple avancé : système de gestion des ressources du parc
Voici un exemple avancé utilisant des types mappés pour créer un système flexible de gestion des ressources de 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}Types mappés intégrés en TypeScript
TypeScript comporte plusieurs types mappés intégrés qui sont très utiles au quotidien :
Partial<T>- Crée un type où toutes les propriétés deTsont optionnelles1type Partial<T> = { [P in keyof T]?: T[P]; };Required<T>- Crée un type où toutes les propriétés deTsont obligatoires1type Required<T> = { [P in keyof T]-?: T[P]; };Readonly<T>- Crée un type où toutes les propriétés deTsont en lecture seule1type Readonly<T> = { readonly [P in keyof T]: T[P]; };Record<K, T>- Crée un type ayant des clésKet des valeurs de typeT1type Record<K extends keyof any, T> = { [P in K]: T; };Pick<T, K>- Sélectionne un sous-ensemble de propriétésKdu typeT1type Pick<T, K extends keyof T> = { [P in K]: T[P]; };Omit<T, K>- Crée un type avec toutes les propriétés deTsauf celles listées dansK1type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;Extract<T, U>- Extrait deTles types qui sont assignables àU1type Extract<T, U> = T extends U ? T : never;Exclude<T, U>- Exclut deTles types qui sont assignables àU1type Exclude<T, U> = T extends U ? never : T;NonNullable<T>- ExclutnulletundefineddeT1type NonNullable<T> = T extends null | undefined ? never : T;Parameters<T>- Extrait les types des paramètres d'un type fonction1type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;ReturnType<T>- Extrait le type de retour d'un type fonction1type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
Ces types mappés intégrés sont extrêmement utiles et permettent des solutions plus concises et élégantes à de nombreux problèmes.
Quand utiliser les types mappés ?
Les types mappés sont particulièrement utiles dans les scénarios suivants :
Transformer des types existants - lorsqu'il faut créer une variante d'un type existant (par ex.
Readonly<T>,Partial<T>)Construire des formulaires et des validations - lors de la création de formulaires dynamiques à partir d'un modèle de données
Configurations et métadonnées - lorsqu'il faut créer une structure de configuration ou de métadonnées pour chaque propriété d'un type
Gérer des API - lorsqu'il faut transformer des types vers un format requis par une API
Générer des types utilitaires - lors de la création de types utilitaires opérant sur d'autres types
Types littéraux de gabarit (template literal types)
TypeScript propose encore un autre mécanisme de type avancé - les types littéraux de gabarit. Ils permettent de créer de nouveaux types chaîne par interpolation, à la manière des gabarits de chaîne (template literals) en JavaScript, mais au niveau des types.
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}`;Les types littéraux de gabarit sont particulièrement utiles pour créer des types précis pour des noms d'événements, des clés de configuration ou des chemins d'API. Ils fonctionnent sur le principe de l'interpolation de chaînes, mais au niveau du système de types - le compilateur TypeScript génère toutes les combinaisons possibles.
Résumé
Les types mappés sont un mécanisme puissant de TypeScript qui permet une transformation systématique des types. Tout comme à Jurassic Park, où les mêmes données de dinosaures peuvent être traitées de différentes manières selon les besoins, les types mappés nous permettent de créer de nouveaux types à partir de types existants, en les adaptant à des exigences précises.
Grâce aux types mappés, nous pouvons créer des API, des formulaires, des validations et bien d'autres structures de données plus flexibles et plus sûrs en matière de types. Ce mécanisme est surtout puissant lorsqu'il est combiné à d'autres fonctionnalités avancées de TypeScript, comme les types conditionnels ou l'inférence de type.
À mesure que tu acquerras de l'expérience avec TypeScript, les types mappés deviendront un outil indispensable de ton arsenal, te permettant de créer des solutions plus expressives, sûres et élégantes.
Code de cette leçon : index.ts
1// Mapped types (Mapped Types) - Parc Jurassique
2console.log("=== MAPPED TYPES ===\n");
3
4// ===========================================
5// 1. Types mappés intégrés
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> - tous les champs en lecture seule
18type FrozenDino = Readonly<DinosaurConfig>;
19
20// Partial<T> - tous les champs optionnels
21type DinoUpdate = Partial<DinosaurConfig>;
22
23// Pick<T, K> - sélection d'un sous-ensemble de champs
24type DinoSummary = Pick<DinosaurConfig, "name" | "species" | "dangerLevel">;
25
26// Omit<T, K> - omission de champs
27type DinoPublic = Omit<DinosaurConfig, "isContained" | "dangerLevel">;
28
29// Record<K, T> - mapping des clés vers un type
30type DinoRegistry = Record<string, DinosaurConfig>;
31
32// ===========================================
33// 2. Types mappés personnalisés
34// ===========================================
35
36// Tous les champs en nullable
37type Nullable<T> = {
38 [K in keyof T]: T[K] | null;
39};
40
41// Tous les champs en optionnel et nullable
42type OptionalNullable<T> = {
43 [K in keyof T]?: T[K] | null;
44};
45
46// Champs numériques convertis en string (formatage)
47type Stringify<T> = {
48 [K in keyof T]: T[K] extends number ? string : T[K];
49};
50
51// Uniquement les champs de type numérique
52type NumericFields<T> = {
53 [K in keyof T as T[K] extends number ? K : never]: T[K];
54};
55
56// Ajout du préfixe 'get' aux noms des champs
57type Getters<T> = {
58 [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
59};
60
61// ===========================================
62// 3. Applications pratiques
63// ===========================================
64
65// Formulaire avec validation - chaque champ a une erreur
66type FormErrors<T> = {
67 [K in keyof T]?: string;
68};
69
70// État de chargement pour chaque champ
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 - mise à jour partielle
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("Après mise à jour:", 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 (personnalisé) ---");
121const maybeDino: Nullable<DinoSummary> = {
122 name: "Unknown",
123 species: null,
124 dangerLevel: null
125};
126console.log("Données partielles:", JSON.stringify(maybeDino));
127
128// Stringify
129console.log("\n--- Stringify (personnalisé) ---");
130const formatted: Stringify<DinoSummary> = {
131 name: "Rexy",
132 species: "T-Rex",
133 dangerLevel: "10/10" // number -> string
134};
135console.log("Formaté:", JSON.stringify(formatted));
136
137// FormErrors
138console.log("\n--- FormErrors ---");
139type DinoForm = Pick<DinosaurConfig, "name" | "weight" | "species">;
140const errors: FormErrors<DinoForm> = {
141 name: "Le nom est requis",
142 weight: "Le poids doit être positif"
143};
144Object.entries(errors).forEach(([field, error]) => {
145 console.log(` ${field}: ${error}`);
146});
147
148console.log("\nMapped types = transformation des types comme des données!");Tu as repéré une erreur dans cette leçon ?
Teste tes connaissances
Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.
1. Que sont les types littéraux de gabarit (template literal types) en TypeScript ?
Tâches pratiques dans le jeu
- Éditeur de code
Crée des types mappés personnalisés pour le système de permissions du parc
- Clique dans l'ordre
Range les éléments du type mappé Readonly :
- Clique dans l'ordre
Range les éléments d'un type basé sur keyof et typeof :
- Tri vertical
Ordonne les étapes de création d'un type utilitaire générique :