Cours JavaScript et TypeScript · Module 7: TypeScript avancé

Contraintes génériques

13 min de lecture
Dans cette leçon9

« À Jurassic Park, tous les dinosaures ne peuvent pas être placés dans les mêmes enclos », explique le Dr Alan Grant, paléontologue en chef du parc. « Les tricératops herbivores peuvent vivre ensemble, mais on ne peut pas les placer avec des vélociraptors prédateurs. Il nous faut des règles claires sur quels dinosaures peuvent être placés où. »

En TypeScript, les contraintes génériques fonctionnent selon un principe similaire - elles permettent de préciser les exigences que doivent remplir les types pouvant être utilisés avec des fonctions ou des classes génériques. Grâce à elles, nous pouvons créer des API plus précises et plus sûres, en garantissant que seuls les types de données appropriés sont acceptés.

Les bases des contraintes génériques

On crée des contraintes génériques à l'aide du mot-clé extends. Cela nous permet de limiter l'éventail des types possibles pouvant être utilisés comme arguments de type.

Exemple : contrainte générique simple

Considérons une fonction censée afficher des informations de base sur un dinosaure. Plutôt que d'accepter n'importe quel type, nous voulons nous assurer que le type passé à la fonction possède au moins les propriétés name et species :

1// Generic constraint - type T must have at least the name and species properties
2function displayDinosaurInfo<T extends { name: string; species: string }>(dino: T): void {
3  console.log(`Name: ${dino.name}, Species: ${dino.species}`);
4}
5
6// Works because the object contains the required properties
7displayDinosaurInfo({ name: "Blue", species: "Velociraptor", age: 5 });
8
9// Also works with more complex objects, as long as they contain the required properties
10displayDinosaurInfo({
11  name: "Rex",
12  species: "Tyrannosaurus Rex",
13  age: 7,
14  weight: 8000,
15  dangerLevel: "Extreme"
16});
17
18// Compilation error - missing required property 'species'
19// displayDinosaurInfo({ name: "Charlie", age: 4 });

Dans cet exemple, T extends { name: string; species: string } signifie que le type générique T doit contenir au moins les propriétés name et species du type approprié. On peut passer un objet doté de propriétés supplémentaires, mais ces deux-là sont obligatoires.

Contraintes sur des interfaces et des types

On définit souvent des interfaces ou des types, puis on les utilise comme contraintes pour nos types génériques :

1// Defining a basic interface
2interface DinosaurBase {
3  id: string;
4  name: string;
5  species: string;
6  age: number;
7}
8
9// Function with a generic constraint based on an interface
10function addToRegistry<T extends DinosaurBase>(dino: T): void {
11  console.log(`Added to registry: ${dino.name} (ID: ${dino.id})`);
12  // Here we could add the dinosaur to the database, etc.
13}
14
15// Works because the object implements DinosaurBase
16addToRegistry({
17  id: "V-001",
18  name: "Delta",
19  species: "Velociraptor",
20  age: 6,
21  trainingLevel: 3 // Additional property is ok
22});
23
24// Compilation error - missing required property 'age'
25// addToRegistry({
26//   id: "T-001",
27//   name: "Rexy",
28//   species: "Tyrannosaurus"
29// });

Contraintes pour les classes génériques

Les contraintes génériques sont particulièrement utiles dans les classes génériques, où l'on veut garantir que les types utilisés avec la classe répondent à des exigences précises :

1// Interface for animals that can be tracked
2interface Trackable {
3  id: string;
4  lastKnownLocation: { x: number; y: number };
5  attachTracker(trackerId: string): void;
6}
7
8// Generic class with a constraint
9class DinosaurTracker<T extends Trackable> {
10  private trackedEntities: T[] = [];
11
12  addEntity(entity: T): void {
13    // We can safely use methods from the Trackable interface
14    entity.attachTracker(`TRK-${Date.now()}`);
15    this.trackedEntities.push(entity);
16    console.log(`Started tracking: ID ${entity.id} at position (${entity.lastKnownLocation.x}, ${entity.lastKnownLocation.y})`);
17  }
18
19  updateLocation(id: string, x: number, y: number): void {
20    const entity = this.trackedEntities.find(e => e.id === id);
21    if (entity) {
22      entity.lastKnownLocation = { x, y };
23      console.log(`Updated position ${id}: (${x}, ${y})`);
24    }
25  }
26
27  getEntities(): readonly T[] {
28    return [...this.trackedEntities];
29  }
30}
31
32// Implementation of a dinosaur class that meets the Trackable interface requirements
33class Velociraptor implements Trackable {
34  id: string;
35  name: string;
36  lastKnownLocation: { x: number; y: number };
37  private tracker: string | null = null;
38
39  constructor(id: string, name: string, initialX: number, initialY: number) {
40    this.id = id;
41    this.name = name;
42    this.lastKnownLocation = { x: initialX, y: initialY };
43  }
44
45  attachTracker(trackerId: string): void {
46    this.tracker = trackerId;
47    console.log(`Attached tracker ${trackerId} to velociraptor ${this.name}`);
48  }
49
50  hunt(): void {
51    // Hunting logic
52    console.log(`${this.name} hunts in the vicinity of (${this.lastKnownLocation.x}, ${this.lastKnownLocation.y})`);
53  }
54}
55
56// Using the tracker with velociraptors
57const raptorTracker = new DinosaurTracker<Velociraptor>();
58
59const blue = new Velociraptor("V-001", "Blue", 120, 340);
60const charlie = new Velociraptor("V-002", "Charlie", 125, 330);
61
62raptorTracker.addEntity(blue);
63raptorTracker.addEntity(charlie);
64
65// We can use Velociraptor-specific methods because we know the concrete type
66const raptors = raptorTracker.getEntities();
67raptors.forEach(raptor => raptor.hunt());
68
69// Simulating movement
70raptorTracker.updateLocation("V-001", 140, 360);

Contraintes à exigences multiples

Parfois, nous avons besoin qu'un type générique satisfasse les exigences de plusieurs interfaces ou types. On peut utiliser le signe & (type intersection) pour combiner des contraintes :

1// Two interfaces representing different aspects of park dynamics
2interface Identifiable {
3  id: string;
4  serialNumber?: string;
5}
6
7interface Statusable {
8  status: "active" | "inactive" | "maintenance";
9  lastStatusUpdate: Date;
10  updateStatus(newStatus: "active" | "inactive" | "maintenance"): void;
11}
12
13// Generic function that requires the type to satisfy both interfaces
14function performMaintenanceCheck<T extends Identifiable & Statusable>(entity: T): void {
15  console.log(`Starting review for ${entity.id}`);
16
17  if (entity.status !== "maintenance") {
18    entity.updateStatus("maintenance");
19    console.log(`Status updated to "maintenance" at ${entity.lastStatusUpdate.toLocaleTimeString()}`);
20  }
21
22  // Simulation of inspection
23  setTimeout(() => {
24    entity.updateStatus("active");
25    console.log(`Review completed for ${entity.id}. Status: active`);
26  }, 2000);
27}
28
29// Class that implements both interfaces
30class SecurityGate implements Identifiable, Statusable {
31  id: string;
32  serialNumber: string;
33  status: "active" | "inactive" | "maintenance";
34  lastStatusUpdate: Date;
35
36  constructor(id: string, serialNumber: string) {
37    this.id = id;
38    this.serialNumber = serialNumber;
39    this.status = "active";
40    this.lastStatusUpdate = new Date();
41  }
42
43  updateStatus(newStatus: "active" | "inactive" | "maintenance"): void {
44    this.status = newStatus;
45    this.lastStatusUpdate = new Date();
46
47    // Additional logic specific to the gate
48    if (newStatus === "active") {
49      console.log(`Gate ${this.id} is now active and secured`);
50    } else if (newStatus === "inactive") {
51      console.log(`WARNING: Gate ${this.id} is inactive. Security threat!`);
52    }
53  }
54
55  // Method specific to the gate
56  toggleLock(locked: boolean): void {
57    console.log(`Gate ${this.id} is now ${locked ? "locked" : "unlocked"}`);
58  }
59}
60
61// Using the function with an object that satisfies both constraints
62const mainGate = new SecurityGate("GATE-001", "SN123456");
63performMaintenanceCheck(mainGate);
64
65// Additional functionality available due to knowing the full type
66mainGate.toggleLock(true);

Contraintes avec éléments variables (keyof)

L'opérateur keyof en TypeScript nous permet d'obtenir l'union des clés d'un objet sous forme de type. On peut le combiner avec des contraintes génériques pour créer des fonctions qui opèrent sur des propriétés précises d'objets :

1// Function that takes an object and a field name, then returns the value of that field
2function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
3  return obj[key];
4}
5
6// Example object with dinosaur data
7const trex = {
8  name: "Rexy",
9  species: "Tyrannosaurus Rex",
10  age: 7,
11  diet: "carnivore",
12  threatLevel: 9,
13  enclosureId: "E-005"
14};
15
16// Correct usage - 'name' is a key of the trex object
17const dinoName = getProperty(trex, "name");
18console.log(dinoName);  // "Rexy"
19
20// Correct usage - 'age' is a key of the trex object
21const dinoAge = getProperty(trex, "age");
22console.log(dinoAge);  // 7
23
24// Compilation error - 'habitat' is not a key of the trex object
25// const habitat = getProperty(trex, "habitat");

Cette contrainte garantit que la clé K est effectivement un nom de propriété de l'objet T. TypeScript sait aussi quel type de valeur sera renvoyé : T[K] (le type de la valeur à la clé K dans un objet de type T).

Exemple pratique : système de reporting pour Jurassic Park

Voyons un exemple plus complexe qui utilise différents aspects des contraintes génériques dans le contexte d'un système de reporting pour Jurassic Park :

1// --- Base type definitions ---
2
3// Base interface for all park resources
4interface ParkResource {
5  id: string;
6  name: string;
7  lastUpdated: Date;
8}
9
10// Interface for report generation
11interface Reportable {
12  generateReport(): string;
13}
14
15// Interface for manageable resources
16interface Manageable {
17  assignTo(employeeId: string): void;
18  getAssignee(): string | null;
19}
20
21// --- Specific resource types ---
22
23interface Dinosaur extends ParkResource {
24  species: string;
25  diet: "carnivore" | "herbivore" | "omnivore";
26  healthStatus: "healthy" | "sick" | "critical";
27  enclosureId: string;
28}
29
30interface Employee extends ParkResource {
31  role: string;
32  department: "research" | "security" | "operations" | "visitor";
33  clearanceLevel: 1 | 2 | 3 | 4 | 5;
34}
35
36interface Facility extends ParkResource {
37  type: "enclosure" | "lab" | "visitor_center" | "power_plant";
38  status: "operational" | "maintenance" | "construction";
39  location: { x: number; y: number };
40}
41
42// --- Reporting system implementation ---
43
44// Generic base class for all repositories
45class ResourceRepository<T extends ParkResource> {
46  protected resources: Map<string, T> = new Map();
47
48  add(resource: T): void {
49    this.resources.set(resource.id, {
50      ...resource,
51      lastUpdated: new Date()
52    });
53  }
54
55  get(id: string): T | undefined {
56    return this.resources.get(id);
57  }
58
59  getAll(): T[] {
60    return Array.from(this.resources.values());
61  }
62
63  update(id: string, updates: Partial<T>): boolean {
64    const resource = this.resources.get(id);
65    if (!resource) return false;
66
67    this.resources.set(id, {
68      ...resource,
69      ...updates,
70      lastUpdated: new Date()
71    });
72
73    return true;
74  }
75
76  delete(id: string): boolean {
77    return this.resources.delete(id);
78  }
79
80  count(): number {
81    return this.resources.size;
82  }
83}
84
85// Class for manageable resources - adds assignment functionality
86class ManageableResourceRepository<T extends ParkResource & Partial<Manageable>>
87  extends ResourceRepository<T> {
88
89  private assignments: Map<string, string> = new Map(); // resourceId -> employeeId
90
91  assignResource(resourceId: string, employeeId: string): boolean {
92    const resource = this.resources.get(resourceId);
93    if (!resource) return false;
94
95    this.assignments.set(resourceId, employeeId);
96
97    // If the resource implements Manageable, we call its assignTo method
98    if (resource.assignTo) {
99      resource.assignTo(employeeId);
100    }
101
102    return true;
103  }
104
105  getAssignee(resourceId: string): string | null {
106    // First, we check if the resource implements its own getAssignee method
107    const resource = this.resources.get(resourceId);
108    if (resource?.getAssignee) {
109      return resource.getAssignee();
110    }
111
112    // Otherwise, we use our internal map
113    return this.assignments.get(resourceId) || null;
114  }
115
116  getAssignedResources(employeeId: string): T[] {
117    const assigned: T[] = [];
118
119    this.assignments.forEach((assignedEmployeeId, resourceId) => {
120      if (assignedEmployeeId === employeeId) {
121        const resource = this.resources.get(resourceId);
122        if (resource) assigned.push(resource);
123      }
124    });
125
126    return assigned;
127  }
128}
129
130// Class for resources that can generate reports
131class ReportableResourceRepository<T extends ParkResource & Reportable>
132  extends ResourceRepository<T> {
133
134  generateIndividualReport(id: string): string | null {
135    const resource = this.resources.get(id);
136    if (!resource) return null;
137
138    return resource.generateReport();
139  }
140
141  generateSummaryReport(): string {
142    const allResources = this.getAll();
143
144    let report = `SUMMARY REPORT
145=======================
146Total resources: ${allResources.length}
147Last update: ${new Date().toLocaleString()}
148=======================\n\n`;
149
150    allResources.forEach(resource => {
151      report += `${resource.name} (ID: ${resource.id}):\n`;
152      report += `${resource.generateReport()}\n\n`;
153    });
154
155    return report;
156  }
157}
158
159// --- Specific repository implementations ---
160
161// Dinosaur repository
162class DinosaurRepository extends ManageableResourceRepository<Dinosaur> {
163  // Methods specific to dinosaurs
164  findBySpecies(species: string): Dinosaur[] {
165    return this.getAll().filter(dino => dino.species === species);
166  }
167
168  findByDiet(diet: Dinosaur["diet"]): Dinosaur[] {
169    return this.getAll().filter(dino => dino.diet === diet);
170  }
171
172  findByEnclosure(enclosureId: string): Dinosaur[] {
173    return this.getAll().filter(dino => dino.enclosureId === enclosureId);
174  }
175
176  // Method generating a special report for dinosaurs
177  generateHealthStatusReport(): string {
178    const allDinos = this.getAll();
179    const healthy = allDinos.filter(d => d.healthStatus === "healthy").length;
180    const sick = allDinos.filter(d => d.healthStatus === "sick").length;
181    const critical = allDinos.filter(d => d.healthStatus === "critical").length;
182
183    return `
184DINOSAUR HEALTH REPORT
185=========================
186Total: ${allDinos.length}
187Healthy: ${healthy} (${Math.round(healthy / allDinos.length * 100)}%)
188Sick: ${sick} (${Math.round(sick / allDinos.length * 100)}%)
189Critical: ${critical} (${Math.round(critical / allDinos.length * 100)}%)
190=========================
191`;
192  }
193}
194
195// Employee repository
196class EmployeeRepository extends ManageableResourceRepository<Employee> {
197  // Methods specific to employees
198  findByDepartment(department: Employee["department"]): Employee[] {
199    return this.getAll().filter(emp => emp.department === department);
200  }
201
202  findByClearanceLevel(level: Employee["clearanceLevel"]): Employee[] {
203    return this.getAll().filter(emp => emp.clearanceLevel === level);
204  }
205}
206
207// --- Example resource classes with reporting implementation ---
208
209// Dinosaur class implementing Reportable
210class ReportableDinosaur implements Dinosaur, Reportable {
211  id: string;
212  name: string;
213  species: string;
214  diet: "carnivore" | "herbivore" | "omnivore";
215  healthStatus: "healthy" | "sick" | "critical";
216  enclosureId: string;
217  lastUpdated: Date;
218
219  // Additional properties
220  private weight: number;
221  private height: number;
222  private behaviors: string[] = [];
223
224  constructor(
225    id: string,
226    name: string,
227    species: string,
228    diet: "carnivore" | "herbivore" | "omnivore",
229    healthStatus: "healthy" | "sick" | "critical",
230    enclosureId: string,
231    weight: number,
232    height: number
233  ) {
234    this.id = id;
235    this.name = name;
236    this.species = species;
237    this.diet = diet;
238    this.healthStatus = healthStatus;
239    this.enclosureId = enclosureId;
240    this.weight = weight;
241    this.height = height;
242    this.lastUpdated = new Date();
243  }
244
245  addBehaviorObservation(behavior: string): void {
246    this.behaviors.push(`${new Date().toISOString()}: ${behavior}`);
247    this.lastUpdated = new Date();
248  }
249
250  generateReport(): string {
251    return `
252--- DINOSAUR REPORT ---
253ID: ${this.id}
254Name: ${this.name}
255Species: ${this.species}
256Diet: ${this.diet}
257Health status: ${this.healthStatus}
258Enclosure: ${this.enclosureId}
259Weight: ${this.weight} kg
260Height: ${this.height} m
261Last behaviors: ${this.behaviors.length > 0 ? '\n  - ' + this.behaviors.slice(-3).join('\n  - ') : 'No saved observations'}
262-----------------------
263`;
264  }
265}
266
267// --- Example usage ---
268
269// Creating a repository for reportable dinosaurs
270const reportableDinoRepo = new ReportableResourceRepository<ReportableDinosaur>();
271
272// Adding dinosaurs to the repository
273const rexReport = new ReportableDinosaur(
274  "D-001",
275  "Rexy",
276  "Tyrannosaurus Rex",
277  "carnivore",
278  "healthy",
279  "E-005",
280  8000,
281  5.2
282);
283
284const triceratopsReport = new ReportableDinosaur(
285  "D-002",
286  "Tricy",
287  "Triceratops",
288  "herbivore",
289  "sick",
290  "E-010",
291  6000,
292  3.0
293);
294
295// Adding behavior observations
296rexReport.addBehaviorObservation("Ate entire cow within 15 minutes");
297rexReport.addBehaviorObservation("Territorial roar towards the Spinosaurus enclosure");
298
299triceratopsReport.addBehaviorObservation("Weaker appetite than usual");
300triceratopsReport.addBehaviorObservation("Signs of fever and drowsiness");
301
302// Adding to repository
303reportableDinoRepo.add(rexReport);
304reportableDinoRepo.add(triceratopsReport);
305
306// Generating reports
307console.log(reportableDinoRepo.generateIndividualReport("D-001"));
308console.log(reportableDinoRepo.generateSummaryReport());

Limites des contraintes génériques

Bien que les contraintes génériques soient un outil puissant, elles ont quelques limites :

  1. Information de type incomplète à l'exécution - TypeScript utilise les contraintes génériques principalement au moment de la compilation. À l'exécution, l'information de type est effacée (effacement de type).

  2. Contraintes nominales vs structurelles - TypeScript utilise un typage structurel, ce qui signifie que des objets ayant la même structure sont considérés comme compatibles, même s'ils portent des noms de type différents. Certains programmeurs habitués au typage nominal (comme en Java ou en C#) peuvent trouver cela surprenant.

  3. Pas de contraintes négatives - On ne peut pas spécifier qu'un type générique NE DOIT PAS avoir certaines propriétés ou NE DOIT PAS implémenter une interface précise.

Malgré ces limites, les contraintes génériques sont extrêmement utiles au quotidien en TypeScript.

Quand appliquer des contraintes génériques ?

Il vaut la peine d'appliquer des contraintes génériques lorsque :

  1. Tu dois écrire des fonctions ou des classes qui travaillent avec différents types, mais qui exigent que ces types possèdent certaines caractéristiques communes.

  2. Tu veux être sûr que des méthodes ou des propriétés précises seront disponibles dans les types utilisés avec tes composants génériques.

  3. Tu dois créer des API précises qui garantissent la sécurité des types tout en conservant de la flexibilité.

  4. Tu implémentes des patrons de conception tels que des dépôts (repositories), des fabriques ou des services, censés fonctionner avec des types de données différents mais apparentés.

Résumé

Les contraintes génériques sont un outil important de TypeScript qui permet de créer des composants à la fois sûrs en matière de types et flexibles. Elles te laissent préciser les exigences que doivent remplir les types utilisés avec des fonctions et des classes génériques, ce qui conduit à des API plus précises et plus fiables.

Tout comme à Jurassic Park, où il nous faut des règles claires sur quels dinosaures peuvent être placés dans quels enclos, les contraintes génériques nous permettent de définir clairement quels types peuvent être utilisés avec nos composants génériques. Cela garantit la sécurité et la prévisibilité de notre code tout en conservant la flexibilité qui fait des génériques un outil si puissant.

Code de cette leçon : index.ts
1// Contraintes génériques (constraints) - Parc Jurassique
2console.log("=== CONTRAINTES GÉNÉRIQUES ===\n");
3
4// ===========================================
5// 1. extends - contrainte de type
6// ===========================================
7
8interface HasName {
9  name: string;
10}
11
12interface HasWeight {
13  weight: number;
14}
15
16// T doit avoir le champ 'name'
17function printName<T extends HasName>(entity: T): void {
18  console.log(`Nom: ${entity.name}`);
19}
20
21// T doit avoir à la fois 'name' et 'weight'
22function getHeaviest<T extends HasName & HasWeight>(items: T[]): T | undefined {
23  if (items.length === 0) return undefined;
24  return items.reduce((max, item) => item.weight > max.weight ? item : max);
25}
26
27// ===========================================
28// 2. keyof - accès sûr aux clés
29// ===========================================
30
31interface DinosaurProfile {
32  name: string;
33  species: string;
34  weight: number;
35  height: number;
36  dangerLevel: number;
37}
38
39// K doit être une clé de T
40function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
41  return obj[key];
42}
43
44// Récupérer plusieurs propriétés à la fois
45function pick<T, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> {
46  const result = {} as Pick<T, K>;
47  keys.forEach(key => {
48    result[key] = obj[key];
49  });
50  return result;
51}
52
53// ===========================================
54// 3. Contraintes avec types par défaut
55// ===========================================
56
57interface Sortable {
58  compareTo?(other: this): number;
59}
60
61function sortEntities<T extends HasName>(
62  items: T[],
63  compareFn: (a: T, b: T) => number = (a, b) => a.name.localeCompare(b.name)
64): T[] {
65  return [...items].sort(compareFn);
66}
67
68// ===========================================
69// 4. Contrainte sur le constructeur
70// ===========================================
71
72interface Constructor<T> {
73  new (...args: any[]): T;
74}
75
76function createInstances<T extends HasName>(
77  Ctor: Constructor<T>,
78  count: number,
79  ...args: any[]
80): T[] {
81  const instances: T[] = [];
82  for (let i = 0; i < count; i++) {
83    instances.push(new Ctor(...args));
84  }
85  return instances;
86}
87
88// ===========================================
89// Test
90// ===========================================
91
92const dinosaurs: DinosaurProfile[] = [
93  { name: "Rexy", species: "T-Rex", weight: 8000, height: 5.5, dangerLevel: 10 },
94  { name: "Blue", species: "Velociraptor", weight: 15, height: 1.8, dangerLevel: 8 },
95  { name: "Patty", species: "Brachiosaurus", weight: 50000, height: 16, dangerLevel: 1 },
96  { name: "Cera", species: "Triceratops", weight: 6000, height: 3, dangerLevel: 3 },
97];
98
99// extends
100console.log("--- extends constraint ---");
101printName({ name: "Rexy" }); // OK - a le champ 'name'
102// printName({ age: 5 }); // Error! - 'name' manquant
103
104const heaviest = getHeaviest(dinosaurs);
105console.log(`Le plus lourd: ${heaviest?.name} (${heaviest?.weight}kg)`);
106
107// keyof
108console.log("\n--- keyof constraint ---");
109const rex = dinosaurs[0];
110console.log("Name:", getProperty(rex, "name"));
111console.log("Weight:", getProperty(rex, "weight"));
112// getProperty(rex, "color"); // Error! 'color' n'est pas une clé
113
114const summary = pick(rex, ["name", "species", "dangerLevel"]);
115console.log("Champs sélectionnés:", JSON.stringify(summary));
116
117// Tri avec contrainte
118console.log("\n--- sortEntities ---");
119const sorted = sortEntities(dinosaurs);
120console.log("Alphabétiquement:", sorted.map(d => d.name).join(", "));
121
122const byDanger = sortEntities(dinosaurs, (a, b) => b.dangerLevel - a.dangerLevel);
123console.log("Par danger:", byDanger.map(d => `${d.name}(${d.dangerLevel})`).join(", "));
124
125// Contraintes multiples
126console.log("\n--- Multiple constraints ---");
127function describeEntity<T extends HasName & HasWeight>(entity: T): string {
128  return `${entity.name} pèse ${entity.weight}kg`;
129}
130dinosaurs.forEach(d => console.log(describeEntity(d)));
131
132console.log("\nConstraints = generics sécurisés avec garantie de types!");

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 signifie la notation <T extends { length: number }> en TypeScript ?

  2. 2. Qu'est-ce qu'une contrainte (constraint) dans les génériques en TypeScript ?

Tâches pratiques dans le jeu

  • Éditeur de code

    Utilise des contraintes pour un accès aux données sûr en matière de types

  • Tri horizontal

    Range les éléments de l'utilisation de keyof :

  • Tri vertical

    Ordonne les contraintes de la plus générale à la plus spécifique :

Articles utiles