Cours JavaScript et TypeScript · Module 7: TypeScript avancé

Types mappés

18 min de lecture
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 :

  • T est le type source
  • K in keyof T parcourt toutes les clés du type T
  • TransformedType est 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 property

Modifier 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; // Error

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

  1. Partial<T> - Crée un type où toutes les propriétés de T sont optionnelles

    1type Partial<T> = { [P in keyof T]?: T[P]; };
  2. Required<T> - Crée un type où toutes les propriétés de T sont obligatoires

    1type Required<T> = { [P in keyof T]-?: T[P]; };
  3. Readonly<T> - Crée un type où toutes les propriétés de T sont en lecture seule

    1type Readonly<T> = { readonly [P in keyof T]: T[P]; };
  4. Record<K, T> - Crée un type ayant des clés K et des valeurs de type T

    1type Record<K extends keyof any, T> = { [P in K]: T; };
  5. Pick<T, K> - Sélectionne un sous-ensemble de propriétés K du type T

    1type Pick<T, K extends keyof T> = { [P in K]: T[P]; };
  6. Omit<T, K> - Crée un type avec toutes les propriétés de T sauf celles listées dans K

    1type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
  7. Extract<T, U> - Extrait de T les types qui sont assignables à U

    1type Extract<T, U> = T extends U ? T : never;
  8. Exclude<T, U> - Exclut de T les types qui sont assignables à U

    1type Exclude<T, U> = T extends U ? never : T;
  9. NonNullable<T> - Exclut null et undefined de T

    1type NonNullable<T> = T extends null | undefined ? never : T;
  10. Parameters<T> - Extrait les types des paramètres d'un type fonction

    1type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;
  11. ReturnType<T> - Extrait le type de retour d'un type fonction

    1type 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 :

  1. Transformer des types existants - lorsqu'il faut créer une variante d'un type existant (par ex. Readonly<T>, Partial<T>)

  2. Construire des formulaires et des validations - lors de la création de formulaires dynamiques à partir d'un modèle de données

  3. 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

  4. Gérer des API - lorsqu'il faut transformer des types vers un format requis par une API

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

Articles utiles