Cours JavaScript et TypeScript · Module 6: Bases de TypeScript

Projet final : le système de gestion typé de Jurassic Park

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

  1. Système de types des dinosaures - enums, interfaces et unions décrivant tous les aspects des dinosaures
  2. Système de gestion des secteurs - interfaces et types pour l'infrastructure du parc
  3. Système de sécurité - types pour les événements, les alertes et les protocoles
  4. 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 arbitrary

Utilise 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 ?

Articles utiles