Cours JavaScript et TypeScript · Module 6: Bases de TypeScript
Projet final : le système de gestion typé de Jurassic Park
Dans cette leçon4
Félicitations, tu as atteint le projet final du module sur les fondamentaux de TypeScript ! Au fil des leçons précédentes, tu as appris de puissants outils de typage : des types de base, en passant par les tableaux, les tuples et les enums, jusqu'aux interfaces, aux types union et aux alias. Il est maintenant temps de rassembler toutes ces connaissances dans un seul projet complet.
Imagine que l'équipe de développement de Jurassic Park a enfin décidé de réécrire ses systèmes en passant du JavaScript pur à TypeScript. Après trop d'incidents causés par des erreurs de type (des paramètres de puissance mal transmis, des espèces de dinosaures confondues, jusqu'à des pannes du système de sécurité à cause d'undefined), la direction a pris une décision : « Plus jamais ça ! » Ta mission consiste à créer les fondations d'un nouveau système de gestion du parc, entièrement typé.
Dans ce projet, tu vas construire un système complet de types et d'interfaces pour Jurassic Park, qui garantira la sécurité des types à chaque niveau de l'application.
Objectif du projet
Tu vas créer un système de gestion typé de Jurassic Park composé de quatre domaines :
- Système de types des dinosaures - enums, interfaces et unions décrivant tous les aspects des dinosaures
- Système de gestion des secteurs - interfaces et types pour l'infrastructure du parc
- Système de sécurité - types pour les événements, les alertes et les protocoles
- Système de gestion centrale - combinant tous les types en un système cohérent
Exigences fonctionnelles
1. Système de types des dinosaures (Enums, interfaces, alias)
Le premier domaine est un modèle de données précis décrivant les dinosaures du parc. On utilise des enums pour les valeurs constantes, des interfaces pour la structure des objets, et des type aliases pour les combinaisons complexes.
1// Enums - constant values in the system
2enum Diet {
3 Carnivore = "carnivore",
4 Herbivore = "herbivore",
5 Omnivore = "omnivore",
6 Piscivore = "piscivore"
7}
8
9enum Era {
10 Triassic = "triassic",
11 Jurassic = "jurassic",
12 Cretaceous = "cretaceous"
13}
14
15enum ThreatLevel {
16 Minimal = 1,
17 Low = 2,
18 Moderate = 3,
19 High = 4,
20 Extreme = 5
21}
22
23enum DinoStatus {
24 Healthy = "healthy",
25 Sick = "sick",
26 Sleeping = "sleeping",
27 Feeding = "feeding",
28 Agitated = "agitated",
29 Escaped = "escaped"
30}
31
32// Type alias - dinosaur species
33type Species =
34 | "Tyrannosaurus Rex"
35 | "Velociraptor"
36 | "Triceratops"
37 | "Brachiosaurus"
38 | "Stegosaurus"
39 | "Pteranodon"
40 | "Dilophosaurus"
41 | "Gallimimus"
42 | "Parasaurolophus"
43 | "Ankylosaurus";
44
45// Interface - dinosaur health data
46interface DinoHealth {
47 weight: number; // in kilograms
48 height: number; // in meters
49 heartRate: number; // beats per minute
50 temperature: number; // degrees Celsius
51 lastFed: Date;
52 lastCheckup: Date;
53 conditions: string[]; // list of health conditions
54}
55
56// Interface - full dinosaur profile
57interface Dinosaur {
58 id: string;
59 name: string;
60 species: Species; // Type alias
61 diet: Diet; // Enum
62 era: Era; // Enum
63 threatLevel: ThreatLevel; // Enum
64 status: DinoStatus; // Enum
65 health: DinoHealth; // Nested interface
66 sectorId: string;
67 trackingChipActive: boolean;
68 dateAdded: Date;
69}
70
71// Union type - dinosaur search filter
72type DinoSearchCriteria =
73 | { by: "species"; value: Species }
74 | { by: "diet"; value: Diet }
75 | { by: "threatLevel"; minLevel: ThreatLevel }
76 | { by: "status"; value: DinoStatus }
77 | { by: "sector"; sectorId: string };
78
79// Function with types
80function searchDinosaurs(
81 dinosaurs: Dinosaur[],
82 criteria: DinoSearchCriteria
83): Dinosaur[] {
84 switch (criteria.by) {
85 case "species":
86 return dinosaurs.filter(d => d.species === criteria.value);
87 case "diet":
88 return dinosaurs.filter(d => d.diet === criteria.value);
89 case "threatLevel":
90 return dinosaurs.filter(d => d.threatLevel >= criteria.minLevel);
91 case "status":
92 return dinosaurs.filter(d => d.status === criteria.value);
93 case "sector":
94 return dinosaurs.filter(d => d.sectorId === criteria.sectorId);
95 }
96}Remarque comment TypeScript impose l'exhaustivité du traitement dans un switch : si tu ajoutes une nouvelle variante à DinoSearchCriteria, le compilateur te rappellera de gérer ce nouveau cas.
2. Système de gestion des secteurs (Interfaces et types complexes)
Le deuxième domaine est le modèle de l'infrastructure du parc : secteurs, clôtures, systèmes d'alimentation et surveillance.
1// Union type - fence type
2type FenceType = "electric" | "concrete" | "reinforced" | "moat";
3
4// Fence interface
5interface Fence {
6 id: string;
7 type: FenceType;
8 voltage: number; // 0 for non-electric
9 integrity: number; // 0-100%
10 lastMaintenance: Date;
11 requiresRepair: boolean;
12}
13
14// Enum - sector status
15enum SectorStatus {
16 Operational = "operational",
17 Maintenance = "maintenance",
18 Lockdown = "lockdown",
19 Evacuated = "evacuated",
20 Offline = "offline"
21}
22
23// Interface - power system
24interface PowerSystem {
25 mainPowerLevel: number; // 0-100%
26 backupPowerLevel: number; // 0-100%
27 isBackupActive: boolean;
28 solarPanelsOnline: number; // number of active panels
29 generatorStatus: "running" | "standby" | "failed";
30}
31
32// Sector interface with nested types
33interface Sector {
34 id: string;
35 name: string;
36 status: SectorStatus;
37 dinosaurs: Dinosaur[];
38 fences: Fence[];
39 power: PowerSystem;
40 capacity: number; // maximum number of dinosaurs
41 securityLevel: ThreatLevel;
42 staff: StaffMember[];
43}
44
45// Type alias - staff role
46type StaffRole =
47 | "veterinarian"
48 | "guard"
49 | "technician"
50 | "scientist"
51 | "manager"
52 | "guide";
53
54// Staff member interface
55interface StaffMember {
56 id: string;
57 name: string;
58 role: StaffRole;
59 assignedSector: string;
60 clearanceLevel: ThreatLevel; // Reusing enum in a different context
61 onDuty: boolean;
62 specializations: string[];
63}
64
65// Type - sector status summary
66type SectorSummary = {
67 sectorId: string;
68 sectorName: string;
69 status: SectorStatus;
70 dinoCount: number;
71 avgFenceIntegrity: number;
72 powerStatus: "stable" | "low" | "critical";
73 staffOnDuty: number;
74 alerts: number;
75};
76
77// Function generating the summary
78function getSectorSummary(sector: Sector): SectorSummary {
79 const avgFence = sector.fences.reduce(
80 (sum, f) => sum + f.integrity, 0
81 ) / sector.fences.length;
82
83 const powerStatus: SectorSummary["powerStatus"] =
84 sector.power.mainPowerLevel > 60 ? "stable" :
85 sector.power.mainPowerLevel > 30 ? "low" : "critical";
86
87 return {
88 sectorId: sector.id,
89 sectorName: sector.name,
90 status: sector.status,
91 dinoCount: sector.dinosaurs.length,
92 avgFenceIntegrity: Math.round(avgFence),
93 powerStatus,
94 staffOnDuty: sector.staff.filter(s => s.onDuty).length,
95 alerts: 0
96 };
97}Les interfaces se construisent de façon hiérarchique : Sector contient des tableaux Dinosaur[], Fence[] et StaffMember[], créant ainsi un modèle de données riche.
3. Système de sécurité (Types union et alias)
Le troisième domaine est le système d'événements de sécurité et d'alertes, qui s'appuie fortement sur les types union pour modéliser précisément différents types d'incidents.
1// Union types for security events - each event has a different structure
2type SecurityEvent =
3 | {
4 type: "fence_breach";
5 sectorId: string;
6 fenceId: string;
7 dinosaurId: string;
8 severity: "warning" | "critical";
9 }
10 | {
11 type: "power_outage";
12 sectorId: string;
13 affectedSystems: string[];
14 backupActivated: boolean;
15 }
16 | {
17 type: "dinosaur_escape";
18 dinosaurId: string;
19 species: Species;
20 lastKnownSector: string;
21 threatLevel: ThreatLevel;
22 }
23 | {
24 type: "sensor_failure";
25 sectorId: string;
26 sensorIds: string[];
27 possibleCause: "damage" | "power" | "sabotage" | "unknown";
28 }
29 | {
30 type: "unauthorized_access";
31 location: string;
32 personId: string | null;
33 timestamp: Date;
34 };
35
36// Enum - alert priority
37enum AlertPriority {
38 Info = "INFO",
39 Warning = "WARNING",
40 Danger = "DANGER",
41 Critical = "CRITICAL",
42 Evacuation = "EVACUATION"
43}
44
45// Alert interface
46interface SecurityAlert {
47 id: string;
48 event: SecurityEvent;
49 priority: AlertPriority;
50 createdAt: Date;
51 acknowledgedBy: string | null; // Staff member ID or null
52 resolved: boolean;
53 notes: string[];
54}
55
56// Type - threat response protocol
57type ResponseProtocol = {
58 alertId: string;
59 actions: ProtocolAction[];
60 estimatedTime: number; // in minutes
61 requiredClearance: ThreatLevel;
62};
63
64// Union type - protocol action
65type ProtocolAction =
66 | { action: "lockdown"; sectors: string[] }
67 | { action: "evacuate"; sectors: string[]; priority: "staff" | "visitors" | "all" }
68 | { action: "deploy_team"; teamType: StaffRole; targetSector: string }
69 | { action: "activate_backup"; systems: string[] }
70 | { action: "track_dinosaur"; dinosaurId: string; useHelicopter: boolean };
71
72// Event handling function with type narrowing
73function handleSecurityEvent(event: SecurityEvent): ResponseProtocol {
74 const actions: ProtocolAction[] = [];
75 let estimatedTime = 10;
76 let requiredClearance = ThreatLevel.Moderate;
77
78 switch (event.type) {
79 case "fence_breach":
80 actions.push({ action: "lockdown", sectors: [event.sectorId] });
81 actions.push({ action: "deploy_team", teamType: "technician", targetSector: event.sectorId });
82 if (event.severity === "critical") {
83 actions.push({ action: "evacuate", sectors: [event.sectorId], priority: "visitors" });
84 requiredClearance = ThreatLevel.High;
85 }
86 break;
87
88 case "dinosaur_escape":
89 actions.push({ action: "track_dinosaur", dinosaurId: event.dinosaurId, useHelicopter: event.threatLevel >= ThreatLevel.High });
90 actions.push({ action: "lockdown", sectors: [event.lastKnownSector] });
91 actions.push({ action: "deploy_team", teamType: "guard", targetSector: event.lastKnownSector });
92 estimatedTime = 60;
93 requiredClearance = ThreatLevel.Extreme;
94 break;
95
96 case "power_outage":
97 actions.push({ action: "activate_backup", systems: event.affectedSystems });
98 actions.push({ action: "deploy_team", teamType: "technician", targetSector: event.sectorId });
99 if (!event.backupActivated) {
100 actions.push({ action: "lockdown", sectors: [event.sectorId] });
101 requiredClearance = ThreatLevel.High;
102 }
103 break;
104
105 case "sensor_failure":
106 actions.push({ action: "deploy_team", teamType: "technician", targetSector: event.sectorId });
107 if (event.possibleCause === "sabotage") {
108 actions.push({ action: "lockdown", sectors: [event.sectorId] });
109 requiredClearance = ThreatLevel.Extreme;
110 }
111 break;
112
113 case "unauthorized_access":
114 actions.push({ action: "deploy_team", teamType: "guard", targetSector: event.location });
115 break;
116 }
117
118 return {
119 alertId: `PROTOCOL-${Date.now()}`,
120 actions,
121 estimatedTime,
122 requiredClearance
123 };
124}Le rétrécissement de type (« type narrowing ») dans un switch permet à TypeScript de restreindre automatiquement le type de l'événement : dans le bloc case "fence_breach", TypeScript sait que event possède les champs fenceId et severity.
4. Système de gestion centrale (Combiner les types)
Le dernier domaine est le système central qui combine tous les types précédents en une interface cohérente de gestion du parc.
1// Interface for the entire park
2interface JurassicPark {
3 name: string;
4 sectors: Sector[];
5 totalDinosaurs: number;
6 activeAlerts: SecurityAlert[];
7 operationalStatus: "open" | "restricted" | "closed" | "emergency";
8}
9
10// Type - daily report
11type DailyReport = {
12 date: string;
13 parkStatus: JurassicPark["operationalStatus"]; // Reusing type from interface
14 sectorSummaries: SectorSummary[];
15 incidentCount: number;
16 resolvedIncidents: number;
17 dinosaurStats: {
18 total: number;
19 byDiet: Record<Diet, number>; // Record with enum as key
20 byStatus: Record<DinoStatus, number>;
21 escaped: number;
22 };
23 staffStats: {
24 totalOnDuty: number;
25 byRole: Record<StaffRole, number>; // Record with type alias as key
26 };
27 recommendations: string[];
28};
29
30// Function generating the report
31function generateDailyReport(park: JurassicPark): DailyReport {
32 // Initialize counters with enums
33 const byDiet: Record<Diet, number> = {
34 [Diet.Carnivore]: 0,
35 [Diet.Herbivore]: 0,
36 [Diet.Omnivore]: 0,
37 [Diet.Piscivore]: 0
38 };
39
40 const byStatus: Record<DinoStatus, number> = {
41 [DinoStatus.Healthy]: 0,
42 [DinoStatus.Sick]: 0,
43 [DinoStatus.Sleeping]: 0,
44 [DinoStatus.Feeding]: 0,
45 [DinoStatus.Agitated]: 0,
46 [DinoStatus.Escaped]: 0
47 };
48
49 // Counting dinosaurs
50 const allDinos = park.sectors.flatMap(s => s.dinosaurs);
51 allDinos.forEach(dino => {
52 byDiet[dino.diet]++;
53 byStatus[dino.status]++;
54 });
55
56 // Sector summaries
57 const sectorSummaries = park.sectors.map(getSectorSummary);
58
59 // Recommendations based on data
60 const recommendations: string[] = [];
61
62 if (byStatus[DinoStatus.Escaped] > 0) {
63 recommendations.push(`CRITICAL: ${byStatus[DinoStatus.Escaped]} dinosaur(s) escaped!`);
64 }
65
66 if (byStatus[DinoStatus.Agitated] > allDinos.length * 0.3) {
67 recommendations.push("WARNING: Over 30% of dinosaurs are agitated - investigate the cause");
68 }
69
70 const lowPowerSectors = sectorSummaries.filter(s => s.powerStatus === "critical");
71 if (lowPowerSectors.length > 0) {
72 recommendations.push(
73 `Critical power level in sectors: ${lowPowerSectors.map(s => s.sectorName).join(", ")}`
74 );
75 }
76
77 return {
78 date: new Date().toISOString().split("T")[0],
79 parkStatus: park.operationalStatus,
80 sectorSummaries,
81 incidentCount: park.activeAlerts.length,
82 resolvedIncidents: park.activeAlerts.filter(a => a.resolved).length,
83 dinosaurStats: {
84 total: allDinos.length,
85 byDiet,
86 byStatus,
87 escaped: byStatus[DinoStatus.Escaped]
88 },
89 staffStats: {
90 totalOnDuty: park.sectors.flatMap(s => s.staff).filter(s => s.onDuty).length,
91 byRole: {
92 veterinarian: 0, guard: 0, technician: 0,
93 scientist: 0, manager: 0, guide: 0
94 }
95 },
96 recommendations
97 };
98}Conseils d'implémentation
Lors de la conception du système de types, garde en tête les principes clés :
Utilise des enums pour les ensembles de valeurs constantes :
1// Good - values are constant and known in advance
2enum Diet { Carnivore = "carnivore", Herbivore = "herbivore" }
3
4// Bad - too dynamic data for an enum
5enum DinosaurName { Rex = "Rex", Blue = "Blue" } // Names can be arbitraryUtilise des interfaces pour les structures d'objets, et des alias pour les unions et les combinaisons :
1// Interface - describes the shape of an object
2interface Dinosaur {
3 name: string;
4 species: Species;
5}
6
7// Alias - combines types, creates variants
8type SearchResult = Dinosaur | null;
9type DinoFilter = (dino: Dinosaur) => boolean;
10type DinoOrError = { success: true; data: Dinosaur } | { success: false; error: string };Utilise des types union discriminés pour les événements :
1// The "type" field allows TypeScript to distinguish variants
2type Event =
3 | { type: "A"; fieldA: string }
4 | { type: "B"; fieldB: number };
5
6function handle(event: Event) {
7 if (event.type === "A") {
8 // TypeScript knows fieldA is here
9 console.log(event.fieldA);
10 }
11}Utilise Record pour créer des correspondances à partir des enums :
1// Automatic enum → value map
2const dietLabels: Record<Diet, string> = {
3 [Diet.Carnivore]: "Carnivore - feed with meat",
4 [Diet.Herbivore]: "Herbivore - feed with plants",
5 [Diet.Omnivore]: "Omnivore - mixed diet",
6 [Diet.Piscivore]: "Piscivore - feed with fish"
7};Bonne chance !
Ce projet est ta chance de prouver que tu comprends toute la puissance du système de types de TypeScript. Dans un vrai Jurassic Park, la moindre erreur peut coûter des vies, et TypeScript est ta clôture qui protège contre les erreurs de type. Comme l'a dit le Dr Alan Grant : « Les dinosaures et l'homme, deux espèces séparées par 65 millions d'années d'évolution, doivent soudain à nouveau cohabiter. » Et TypeScript rend cette cohabitation sûre et prévisible.
Crée ton système de gestion typé dans l'éditeur ci-dessous. Définis au moins trois enums, quatre interfaces, deux types union et une fonction utilisant ces types.
Code de cette leçon : index.ts
1// Projet final: Système de gestion typé du Parc Jurassique
2// Combine: TypeScript types, interfaces, enums, unions, aliases
3
4// === 1. ENUMS ET CONSTANTES ===
5// TODO: Définis des enums pour le système du parc
6enum Diet {
7 Carnivore = "carnivore",
8 Herbivore = "herbivore",
9 Omnivore = "omnivore",
10}
11
12enum ThreatLevel {
13 Minimal = 1,
14 Low = 2,
15 Moderate = 3,
16 High = 4,
17 Extreme = 5,
18}
19
20// TODO: Add les enums DinoStatus et Era
21
22// === 2. INTERFACES ===
23// TODO: Crée l'interface IDinosaur
24interface IDinosaur {
25 id: string;
26 name: string;
27 species: string;
28 diet: Diet;
29 weight: number;
30 threatLevel: ThreatLevel;
31 // TODO: Add les champs restants: isHealthy, enclosure, era
32}
33
34// TODO: Crée l'interface IEnclosure
35interface IEnclosure {
36 id: string;
37 name: string;
38 // TODO: Add des champs: capacity, currentOccupants, securityLevel, isElectrified
39}
40
41// === 3. TYPES UNION ET ALIASES ===
42// TODO: Crée le type AlertType = "breach" | "health" | "power" | "weather"
43// TODO: Crée le type SecurityEvent avec discriminated union
44
45// === 4. FUNCTIONS AVEC DES TYPES ===
46// TODO: Crée une function typée registerDinosaur
47function registerDinosaur(data: Omit<IDinosaur, "id">): IDinosaur {
48 // TODO: Add un ID automatique et retourne l'object complet
49 return { id: "DINO-001", ...data };
50}
51
52// TODO: Crée une function assignToEnclosure(dino: IDinosaur, enclosure: IEnclosure): boolean
53// TODO: Crée une function generateSecurityReport(dinosaurs: IDinosaur[]): string
54
55// === 5. TEST DU SYSTÈME ===
56// TODO: Crée plusieurs dinosaurs et assigne-les à des enclosures
57// TODO: Génère un rapport de sécurité
58
59console.log("Système de gestion typé démarré !");Tu as repéré une erreur dans cette leçon ?