Cours JavaScript et TypeScript · Module 7: TypeScript avancé
Classes génériques
Dans cette leçon9
« Chaque enclos de Jurassic Park doit être adapté à une espèce précise de dinosaure », explique le Dr Ellie Sattler, responsable du département de recherche botanique. « Mais malgré les différences, tous les enclos fonctionnent selon les mêmes principes - contrôle des entrées, surveillance des conditions environnementales, systèmes d'alimentation automatisés. Ce qui distingue un enclos de vélociraptors d'un enclos de tricératops, ce n'est pas sa structure, mais ses paramètres. »
Les classes génériques en TypeScript fonctionnent de la même manière. Elles permettent de créer des composants réutilisables capables de travailler avec différents types de données tout en conservant un contrôle complet des types. Si les fonctions génériques nous ont montré l'idée de base des génériques, les classes génériques en démontrent tout le potentiel.
Que sont les classes génériques ?
Les classes génériques sont des modèles de classes capables de travailler avec différents types de données. Tout comme les fonctions génériques, elles utilisent des paramètres de type (par ex. <T>), mais elles permettent d'utiliser ces paramètres dans toute la classe - dans les champs, les méthodes et les constructeurs.
Voici une classe générique simple représentant un enclos de dinosaures :
1class DinosaurEnclosure<T> {
2 private inhabitants: T[];
3
4 constructor() {
5 this.inhabitants = [];
6 }
7
8 addInhabitant(dino: T): void {
9 this.inhabitants.push(dino);
10 console.log("Added new inhabitant to enclosure!");
11 }
12
13 getInhabitants(): T[] {
14 return [...this.inhabitants]; // We return a copy to avoid direct modification
15 }
16
17 countInhabitants(): number {
18 return this.inhabitants.length;
19 }
20}La classe ci-dessus peut être utilisée pour créer un enclos pour n'importe quel type de dinosaures :
1// Dinosaur type definitions
2interface Velociraptor {
3 name: string;
4 speed: number;
5 clawLength: number;
6}
7
8interface Triceratops {
9 name: string;
10 hornLength: number;
11 weight: number;
12}
13
14// Creating specialized enclosures
15const raptorEnclosure = new DinosaurEnclosure<Velociraptor>();
16const triceratopsEnclosure = new DinosaurEnclosure<Triceratops>();
17
18// Adding inhabitants to enclosures
19raptorEnclosure.addInhabitant({
20 name: "Blue",
21 speed: 70,
22 clawLength: 15
23});
24
25triceratopsEnclosure.addInhabitant({
26 name: "Topsy",
27 hornLength: 80,
28 weight: 9000
29});
30
31// Checking inhabitants
32console.log(`Velociraptor count: ${raptorEnclosure.countInhabitants()}`);
33console.log(`Triceratops count: ${triceratopsEnclosure.countInhabitants()}`);
34
35// TypeScript ensures type safety
36// This won't compile:
37// raptorEnclosure.addInhabitant({
38// name: "Invalid",
39// hornLength: 50,
40// weight: 7000
41// });Classes à plusieurs paramètres de type
Les classes génériques peuvent utiliser plusieurs paramètres de type, ce qui offre une flexibilité encore plus grande :
1class ParkResearchLab<TSpecimen, TResult> {
2 private specimens: TSpecimen[] = [];
3 private results: Map<string, TResult> = new Map();
4
5 addSpecimen(id: string, specimen: TSpecimen): void {
6 this.specimens.push(specimen);
7 console.log(`Added sample #${id}`);
8 }
9
10 recordResult(specimenId: string, result: TResult): void {
11 this.results.set(specimenId, result);
12 console.log(`Saved result for sample #${specimenId}`);
13 }
14
15 getResult(specimenId: string): TResult | undefined {
16 return this.results.get(specimenId);
17 }
18}
19
20// Example usage with specific types
21interface BloodSample {
22 dinosaurId: string;
23 extractionDate: Date;
24 volume: number;
25}
26
27interface BloodTestResult {
28 oxygenLevel: number;
29 whiteBloodCellCount: number;
30 isHealthy: boolean;
31}
32
33const bloodLab = new ParkResearchLab<BloodSample, BloodTestResult>();
34
35bloodLab.addSpecimen("BLD-001", {
36 dinosaurId: "TREX-01",
37 extractionDate: new Date(2023, 4, 15),
38 volume: 250
39});
40
41bloodLab.recordResult("BLD-001", {
42 oxygenLevel: 95,
43 whiteBloodCellCount: 7500,
44 isHealthy: true
45});
46
47const result = bloodLab.getResult("BLD-001");
48console.log(`Blood test result: ${result?.isHealthy ? "Healthy" : "Needs attention"}`);Étendre des classes génériques
Les classes génériques peuvent aussi être étendues, ce qui permet des implémentations plus spécialisées :
1class MonitoredEnclosure<T> extends DinosaurEnclosure<T> {
2 private lastCheckTime: Date | null = null;
3 private securityLevel: "low" | "medium" | "high";
4
5 constructor(securityLevel: "low" | "medium" | "high" = "medium") {
6 super(); // Calling the base class constructor
7 this.securityLevel = securityLevel;
8 }
9
10 performSecurityCheck(): void {
11 this.lastCheckTime = new Date();
12 console.log(`Security check performed at ${this.lastCheckTime.toLocaleTimeString()}`);
13 console.log(`Security level: ${this.securityLevel}`);
14 console.log(`Inhabitant count: ${this.countInhabitants()}`);
15 }
16
17 getLastCheckTime(): Date | null {
18 return this.lastCheckTime;
19 }
20
21 upgradeSecurityLevel(): void {
22 if (this.securityLevel === "low") {
23 this.securityLevel = "medium";
24 } else if (this.securityLevel === "medium") {
25 this.securityLevel = "high";
26 }
27 console.log(`Updated security level to: ${this.securityLevel}`);
28 }
29}
30
31// Using the extended class
32const monitoredRaptorEnclosure = new MonitoredEnclosure<Velociraptor>("high");
33
34monitoredRaptorEnclosure.addInhabitant({
35 name: "Delta",
36 speed: 65,
37 clawLength: 14
38});
39
40monitoredRaptorEnclosure.performSecurityCheck();Paramètres de type par défaut
Tout comme les fonctions peuvent avoir des paramètres par défaut, les classes génériques peuvent avoir des types par défaut pour leurs paramètres de type :
1// Default dinosaur interface
2interface DefaultDinosaur {
3 name: string;
4 species: string;
5 age: number;
6}
7
8// Class with a default type parameter
9class DinosaurTracker<T = DefaultDinosaur> {
10 private trackedDinos: T[] = [];
11
12 trackDinosaur(dino: T, location: string): void {
13 this.trackedDinos.push(dino);
14 console.log(`Started tracking dinosaur at location: ${location}`);
15 }
16
17 getTrackedCount(): number {
18 return this.trackedDinos.length;
19 }
20}
21
22// Usage with default type
23const defaultTracker = new DinosaurTracker();
24defaultTracker.trackDinosaur({
25 name: "Rexy",
26 species: "Tyrannosaurus Rex",
27 age: 7
28}, "Sector B");
29
30// Usage with a custom type
31interface AquaticDinosaur {
32 name: string;
33 underwaterSpeedKmh: number;
34 maxDiveDepthMeters: number;
35}
36
37const aquaticTracker = new DinosaurTracker<AquaticDinosaur>();
38aquaticTracker.trackDinosaur({
39 name: "Nessie",
40 underwaterSpeedKmh: 40,
41 maxDiveDepthMeters: 500
42}, "Central Lake");Propriétés statiques dans les classes génériques
Il est important de comprendre que les paramètres de type dans les classes génériques ne s'appliquent qu'aux propriétés et méthodes d'instance. Les propriétés et méthodes statiques sont partagées entre toutes les instances de la classe, quels que soient leurs paramètres de type :
1class DinosaurRegistry<T> {
2 private static totalRegisteredDinos: number = 0;
3 private registeredDinos: T[] = [];
4
5 registerDinosaur(dino: T): void {
6 this.registeredDinos.push(dino);
7 DinosaurRegistry.totalRegisteredDinos++;
8 console.log(`Registered new dinosaur. Total: ${DinosaurRegistry.totalRegisteredDinos}`);
9 }
10
11 static getTotalRegisteredCount(): number {
12 return DinosaurRegistry.totalRegisteredDinos;
13 }
14}
15
16const rexRegistry = new DinosaurRegistry<Velociraptor>();
17const triRegistry = new DinosaurRegistry<Triceratops>();
18
19rexRegistry.registerDinosaur({
20 name: "Charlie",
21 speed: 68,
22 clawLength: 12
23});
24
25triRegistry.registerDinosaur({
26 name: "Cera",
27 hornLength: 75,
28 weight: 8500
29});
30
31console.log(`Total registered dinosaurs: ${DinosaurRegistry.getTotalRegisteredCount()}`); // 2Dans cet exemple, la variable statique totalRegisteredDinos est partagée entre toutes les instances de DinosaurRegistry, quels que soient leurs paramètres de type.
Classes génériques et interfaces génériques
Les classes génériques travaillent souvent avec des interfaces génériques, formant un système de types complet :
1// Generic interface
2interface Repository<T> {
3 add(item: T): void;
4 getById(id: string): T | undefined;
5 getAll(): T[];
6 update(id: string, item: T): boolean;
7 delete(id: string): boolean;
8}
9
10// Implementation of a generic class conforming to a generic interface
11class DinosaurRepository<T extends { id: string }> implements Repository<T> {
12 private items: Map<string, T> = new Map();
13
14 add(item: T): void {
15 this.items.set(item.id, item);
16 }
17
18 getById(id: string): T | undefined {
19 return this.items.get(id);
20 }
21
22 getAll(): T[] {
23 return Array.from(this.items.values());
24 }
25
26 update(id: string, item: T): boolean {
27 if (this.items.has(id)) {
28 this.items.set(id, item);
29 return true;
30 }
31 return false;
32 }
33
34 delete(id: string): boolean {
35 return this.items.delete(id);
36 }
37}
38
39// Using the repository with a specific type
40interface DinosaurRecord {
41 id: string;
42 name: string;
43 species: string;
44 dateOfBirth: Date;
45}
46
47const dinoRepo = new DinosaurRepository<DinosaurRecord>();
48
49dinoRepo.add({
50 id: "DINO-001",
51 name: "Rex",
52 species: "Tyrannosaurus",
53 dateOfBirth: new Date(2018, 3, 10)
54});
55
56const rex = dinoRepo.getById("DINO-001");
57console.log(`Found dinosaur: ${rex?.name}, Species: ${rex?.species}`);Exemple pratique : système de gestion des actifs de Jurassic Park
Élargissons maintenant nos connaissances des classes génériques en implémentant un système complet de gestion des actifs de Jurassic Park :
1// Basic interface for all park assets
2interface ParkAsset {
3 id: string;
4 name: string;
5 status: "operational" | "maintenance" | "offline";
6 lastUpdated: Date;
7}
8
9// Different asset types
10interface Vehicle extends ParkAsset {
11 type: "jeep" | "boat" | "helicopter";
12 passengerCapacity: number;
13 currentFuel: number;
14}
15
16interface Facility extends ParkAsset {
17 type: "visitor" | "research" | "security";
18 location: { x: number, y: number };
19 capacity: number;
20}
21
22interface PowerSystem extends ParkAsset {
23 type: "main" | "backup" | "sector";
24 outputCapacity: number;
25 currentLoad: number;
26}
27
28// Generic interface for asset management
29interface AssetManager<T extends ParkAsset> {
30 registerAsset(asset: T): void;
31 getAsset(id: string): T | undefined;
32 updateStatus(id: string, status: T["status"]): boolean;
33 getOperationalAssets(): T[];
34}
35
36// Implementation of a generic asset management class
37class JurassicParkAssetManager<T extends ParkAsset> implements AssetManager<T> {
38 private assets: Map<string, T> = new Map();
39 private statusChangeLog: { assetId: string, oldStatus: string, newStatus: string, timestamp: Date }[] = [];
40
41 constructor(private assetType: string) {
42 console.log(`Initialized asset management system for type: ${assetType}`);
43 }
44
45 registerAsset(asset: T): void {
46 if (this.assets.has(asset.id)) {
47 throw new Error(`Asset with ID ${asset.id} already exists`);
48 }
49
50 this.assets.set(asset.id, {
51 ...asset,
52 lastUpdated: new Date()
53 });
54
55 console.log(`Registered new asset: ${asset.name} (ID: ${asset.id})`);
56 }
57
58 getAsset(id: string): T | undefined {
59 return this.assets.get(id);
60 }
61
62 updateStatus(id: string, status: T["status"]): boolean {
63 const asset = this.assets.get(id);
64
65 if (!asset) {
66 console.warn(`Asset with ID ${id} not found`);
67 return false;
68 }
69
70 const oldStatus = asset.status;
71
72 // Status update
73 const updatedAsset = {
74 ...asset,
75 status,
76 lastUpdated: new Date()
77 };
78
79 this.assets.set(id, updatedAsset);
80
81 // Logging the change
82 this.statusChangeLog.push({
83 assetId: id,
84 oldStatus,
85 newStatus: status,
86 timestamp: new Date()
87 });
88
89 console.log(`Updated status of asset ${asset.name} from "${oldStatus}" to "${status}"`);
90 return true;
91 }
92
93 getOperationalAssets(): T[] {
94 return Array.from(this.assets.values())
95 .filter(asset => asset.status === "operational");
96 }
97
98 getStatusChangeHistory(id?: string): typeof this.statusChangeLog {
99 if (id) {
100 return this.statusChangeLog.filter(log => log.assetId === id);
101 }
102 return [...this.statusChangeLog];
103 }
104
105 generateReport(): string {
106 const totalAssets = this.assets.size;
107 const operational = this.getOperationalAssets().length;
108 const maintenance = Array.from(this.assets.values())
109 .filter(asset => asset.status === "maintenance").length;
110 const offline = Array.from(this.assets.values())
111 .filter(asset => asset.status === "offline").length;
112
113 return `
114ASSET STATUS REPORT: ${this.assetType.toUpperCase()}
115======================================
116Total count: ${totalAssets}
117Operational: ${operational} (${Math.round(operational / totalAssets * 100)}%)
118In maintenance: ${maintenance} (${Math.round(maintenance / totalAssets * 100)}%)
119Offline: ${offline} (${Math.round(offline / totalAssets * 100)}%)
120======================================
121Generated: ${new Date().toLocaleString()}
122 `;
123 }
124}
125
126// Using the asset management system with different types
127const vehicleManager = new JurassicParkAssetManager<Vehicle>("Vehicles");
128const facilityManager = new JurassicParkAssetManager<Facility>("Objects");
129const powerManager = new JurassicParkAssetManager<PowerSystem>("Power Supply");
130
131// Registering sample assets
132vehicleManager.registerAsset({
133 id: "VEH-001",
134 name: "Explorer 04",
135 status: "operational",
136 lastUpdated: new Date(),
137 type: "jeep",
138 passengerCapacity: 5,
139 currentFuel: 75
140});
141
142vehicleManager.registerAsset({
143 id: "VEH-002",
144 name: "Explorer 05",
145 status: "maintenance",
146 lastUpdated: new Date(),
147 type: "jeep",
148 passengerCapacity: 5,
149 currentFuel: 30
150});
151
152facilityManager.registerAsset({
153 id: "FAC-001",
154 name: "Visitor Center",
155 status: "operational",
156 lastUpdated: new Date(),
157 type: "visitor",
158 location: { x: 100, y: 200 },
159 capacity: 200
160});
161
162powerManager.registerAsset({
163 id: "PWR-001",
164 name: "Main Power Grid",
165 status: "operational",
166 lastUpdated: new Date(),
167 type: "main",
168 outputCapacity: 10000,
169 currentLoad: 6500
170});
171
172// Simulating failures and status updates
173vehicleManager.updateStatus("VEH-001", "maintenance");
174powerManager.updateStatus("PWR-001", "offline");
175facilityManager.updateStatus("FAC-001", "maintenance");
176
177// After the fix
178setTimeout(() => {
179 vehicleManager.updateStatus("VEH-001", "operational");
180 powerManager.updateStatus("PWR-001", "operational");
181 facilityManager.updateStatus("FAC-001", "operational");
182
183 // Generating reports
184 console.log(vehicleManager.generateReport());
185 console.log(powerManager.generateReport());
186
187 // Status change history
188 console.log("Main power status change history:");
189 console.log(powerManager.getStatusChangeHistory("PWR-001"));
190}, 3000);Quand utiliser les classes génériques ?
Les classes génériques sont particulièrement utiles dans les situations suivantes :
- Collections et conteneurs de données - lorsqu'il faut stocker et gérer des éléments de différents types
- Services et dépôts (repositories) - lors de l'implémentation de patrons de conception qui doivent fonctionner avec différents types de données
- Composants réutilisables - lorsqu'on veut créer des composants flexibles utilisables dans différents contextes
- Abstractions à différents niveaux - lorsqu'on veut définir des hiérarchies de classes spécialisables pour différents types
Résumé
Les classes génériques sont un outil puissant de TypeScript qui permet de créer un code flexible et sûr en matière de types. Elles offrent un excellent équilibre entre réutilisabilité et sécurité des types, en permettant de créer des composants qui :
- Peuvent travailler avec différents types de données
- Conservent toute l'information de type
- Bénéficient du support du compilateur et de l'IDE (suggestions, vérification de types)
- Restent lisibles et compréhensibles pour les autres programmeurs
Tout comme à Jurassic Park, où l'infrastructure doit être flexible pour accueillir différentes espèces de dinosaures, les classes génériques nous permettent de construire des systèmes à la fois flexibles et sûrs - la combinaison parfaite pour le développement logiciel moderne.
Dans le prochain exercice, nous verrons comment affiner davantage nos génériques à l'aide de contraintes génériques, afin de garantir que nos classes et fonctions génériques ne fonctionnent qu'avec des types répondant à des exigences précises.
Code de cette leçon : index.ts
1// Classes génériques - Parc Jurassique
2console.log("=== CLASSES GÉNÉRIQUES ===\n");
3
4// ===========================================
5// 1. GenericRepository<T> - repozytorium danych
6// ===========================================
7
8interface Identifiable {
9 id: number;
10 name: string;
11}
12
13class GenericRepository<T extends Identifiable> {
14 private items: Map<number, T> = new Map();
15 private label: string;
16
17 constructor(label: string) {
18 this.label = label;
19 }
20
21 add(item: T): void {
22 this.items.set(item.id, item);
23 }
24
25 getById(id: number): T | undefined {
26 return this.items.get(id);
27 }
28
29 getAll(): T[] {
30 return Array.from(this.items.values());
31 }
32
33 remove(id: number): boolean {
34 return this.items.delete(id);
35 }
36
37 count(): number {
38 return this.items.size;
39 }
40
41 find(predicate: (item: T) => boolean): T[] {
42 return this.getAll().filter(predicate);
43 }
44
45 toString(): string {
46 return `[${this.label}] ${this.count()} enregistrements`;
47 }
48}
49
50// ===========================================
51// 2. Collection<T> - collection avec itération
52// ===========================================
53
54class Collection<T> {
55 private elements: T[] = [];
56
57 add(...items: T[]): Collection<T> {
58 this.elements.push(...items);
59 return this; // Fluent API
60 }
61
62 first(): T | undefined {
63 return this.elements[0];
64 }
65
66 last(): T | undefined {
67 return this.elements[this.elements.length - 1];
68 }
69
70 map<U>(fn: (item: T) => U): Collection<U> {
71 const result = new Collection<U>();
72 result.add(...this.elements.map(fn));
73 return result;
74 }
75
76 filter(fn: (item: T) => boolean): Collection<T> {
77 const result = new Collection<T>();
78 result.add(...this.elements.filter(fn));
79 return result;
80 }
81
82 toArray(): T[] {
83 return [...this.elements];
84 }
85
86 get size(): number {
87 return this.elements.length;
88 }
89}
90
91// ===========================================
92// 3. Pair<A, B> - paire de valeurs
93// ===========================================
94
95class Pair<A, B> {
96 constructor(
97 public readonly first: A,
98 public readonly second: B
99 ) {}
100
101 swap(): Pair<B, A> {
102 return new Pair(this.second, this.first);
103 }
104
105 toString(): string {
106 return `(${this.first}, ${this.second})`;
107 }
108}
109
110// ===========================================
111// Test
112// ===========================================
113
114// --- GenericRepository ---
115interface Dinosaur {
116 id: number;
117 name: string;
118 species: string;
119 weight: number;
120}
121
122interface Enclosure {
123 id: number;
124 name: string;
125 capacity: number;
126}
127
128const dinoRepo = new GenericRepository<Dinosaur>("Dinosaures");
129dinoRepo.add({ id: 1, name: "Rexy", species: "T-Rex", weight: 8000 });
130dinoRepo.add({ id: 2, name: "Blue", species: "Velociraptor", weight: 15 });
131dinoRepo.add({ id: 3, name: "Cera", species: "Triceratops", weight: 6000 });
132
133const encRepo = new GenericRepository<Enclosure>("Enclos");
134encRepo.add({ id: 101, name: "Zone T-Rex", capacity: 2 });
135encRepo.add({ id: 102, name: "Vallée des Raptors", capacity: 6 });
136
137console.log(dinoRepo.toString());
138console.log(encRepo.toString());
139
140console.log("\nDinosaur #2:", dinoRepo.getById(2)?.name);
141console.log("Lourds (>1000kg):", dinoRepo.find(d => d.weight > 1000).map(d => d.name).join(", "));
142
143// --- Collection ---
144console.log("\n--- Collection avec fluent API ---");
145const names = new Collection<string>()
146 .add("Rexy", "Blue", "Delta", "Charlie", "Cera")
147 .filter(n => n.length <= 4)
148 .map(n => n.toUpperCase());
149
150console.log("Noms courts:", names.toArray().join(", "));
151console.log("Premier:", names.first());
152console.log("Dernier:", names.last());
153
154// --- Pair ---
155console.log("\n--- Pair ---");
156const dinoEnclosure = new Pair<string, string>("Rexy", "Zone T-Rex");
157console.log("Paire:", dinoEnclosure.toString());
158
159const swapped = dinoEnclosure.swap();
160console.log("Échangée:", swapped.toString());
161
162const idWeight = new Pair<number, number>(1, 8000);
163console.log("ID-Poids:", idWeight.toString());
164
165console.log("\nLes classes génériques garantissent la sécurité des 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. Quand vaut-il mieux utiliser un générique <T> plutôt qu'un type union (string | number) ?
Tâches pratiques dans le jeu
- Éditeur de code
Crée un dépôt de données générique pour le parc
- Tri vertical
Ordonne les modificateurs d'accès du plus restrictif au plus large :