Cours JavaScript et TypeScript · Module 7: TypeScript avancé

Implémenter des interfaces

25 min de lecture
Dans cette leçon10

« À Jurassic Park, tout doit fonctionner selon des protocoles stricts », explique Claire Dearing, la directrice des opérations. « Chaque employé a un périmètre de responsabilités clairement défini, et chaque système doit répondre à des exigences précises. Peu nous importe comment exactement un garde surveille l'enclos des raptors - ce qui compte, c'est qu'il fasse son travail conformément aux procédures de sécurité. »

Dans le monde de TypeScript, les interfaces jouent un rôle similaire - elles définissent un « contrat » que les classes doivent respecter, sans préciser comment exactement elles doivent le faire. Dans cet exercice, tu apprendras à concevoir et à implémenter des interfaces en créant un système avancé de gestion pour Jurassic Park.

Les bases des interfaces

Une interface en TypeScript est une structure qui définit la forme d'un objet - quelles propriétés et méthodes il doit avoir, sans préciser leur implémentation. Tu peux voir une interface comme un contrat ou un protocole qu'une classe doit respecter.

Regardons la syntaxe de base d'une interface :

1// Interface definition
2interface ParkEmployee {
3  // Properties
4  id: string;
5  firstName: string;
6  lastName: string;
7  accessLevel: number;
8
9  // Methods
10  login(): void;
11  logout(): void;
12  checkPermissions(requiredLevel: number): boolean;
13}
14
15// Class implementing the interface
16class GuardParku implements ParkEmployee {
17  // Implementation of all required properties
18  id: string;
19  firstName: string;
20  lastName: string;
21  accessLevel: number;
22  private activeStatus: boolean = false;
23
24  constructor(id: string, firstName: string, lastName: string) {
25    this.id = id;
26    this.firstName = firstName;
27    this.lastName = lastName;
28    this.accessLevel = 2; // Niveau standard pour le guard
29  }
30
31  // Implementation of all required methods
32  login(): void {
33    console.log(`Guard ${this.firstName} ${this.lastName} logged into the system.`);
34    this.activeStatus = true;
35  }
36
37  logout(): void {
38    console.log(`Guard ${this.firstName} ${this.lastName} logged out of the system.`);
39    this.activeStatus = false;
40  }
41
42  checkPermissions(requiredLevel: number): boolean {
43    return this.accessLevel >= requiredLevel;
44  }
45
46  // Additional methods specific to the guard
47  patrolSector(sectorId: string): void {
48    if (!this.activeStatus) {
49      console.log("Error: Guard must be logged in to patrol sector.");
50      return;
51    }
52
53    console.log(`Guard ${this.firstName} ${this.lastName} is patrolling sector ${sectorId}.`);
54  }
55}
56
57// Another class implementing the same interface
58class ScientistParku implements ParkEmployee {
59  id: string;
60  firstName: string;
61  lastName: string;
62  accessLevel: number;
63  private specialization: string;
64
65  constructor(id: string, firstName: string, lastName: string, specialization: string) {
66    this.id = id;
67    this.firstName = firstName;
68    this.lastName = lastName;
69    this.accessLevel = 4; // Higher access level for scientists
70    this.specialization = specialization;
71  }
72
73  login(): void {
74    console.log(`Dr ${this.firstName} ${this.lastName} logged into the laboratory system.`);
75  }
76
77  logout(): void {
78    console.log(`Dr ${this.firstName} ${this.lastName} logged out of the laboratory system.`);
79  }
80
81  checkPermissions(requiredLevel: number): boolean {
82    // Scientists have special permissions in laboratories
83    if (requiredLevel === 3 && this.specialization === "genetics") {
84      return true;
85    }
86    return this.accessLevel >= requiredLevel;
87  }
88
89  // Method specific to the scientist
90  conductResearch(researchSubject: string): void {
91    console.log(`Dr ${this.firstName} ${this.lastName} is conducting research on: ${researchSubject}.`);
92  }
93}
94
95// Using classes implementing the interface
96const guard = new GuardParku("SEC-001", "John", "Smith");
97const scientist = new ScientistParku("SCI-042", "Henry", "Wu", "genetics");
98
99// Both classes implement the same interface, so we can use the same methods
100guard.login();
101scientist.login();
102
103// Checking permissions
104console.log(`Guard has access to zone 2: ${guard.checkPermissions(2)}`);
105console.log(`Scientist has access to zone 3: ${scientist.checkPermissions(3)}`);
106
107// Calling methods specific to particular classes
108guard.patrolSector("B-5");
109scientist.conductResearch("DNA Velociraptor");

Dans l'exemple ci-dessus :

  1. Nous avons défini l'interface ParkEmployee précisant quelles propriétés et méthodes chaque employé doit avoir
  2. Nous avons créé deux classes différentes implémentant cette interface - chacune à sa manière
  3. Les deux classes doivent fournir toutes les propriétés et méthodes requises, mais peuvent les implémenter différemment
  4. Les classes peuvent aussi ajouter leurs propres méthodes et propriétés supplémentaires

Propriétés optionnelles et en lecture seule

Les interfaces TypeScript permettent des définitions de propriétés plus avancées :

1interface SecuritySystem {
2  // Required property
3  id: string;
4
5  // Read-only property - cannot be modified after initialization
6  readonly systemType: "monitoring" | "fence" | "motionSensor" | "alarm";
7
8  // Optional property - may or may not exist
9  advancedOptions?: Record<string, any>;
10
11  // Methods
12  activate(): void;
13  deactivate(): void;
14  checkStatus(): string;
15}
16
17class FenceSystem implements SecuritySystem {
18  id: string;
19  readonly systemType: "monitoring" | "fence" | "motionSensor" | "alarm" = "fence";
20  advancedOptions?: Record<string, any>; // Optional property
21
22  private voltage: number = 0;
23  private active: boolean = false;
24
25  constructor(id: string, advancedOptions?: Record<string, any>) {
26    this.id = id;
27    if (advancedOptions) {
28      this.advancedOptions = advancedOptions;
29    }
30  }
31
32  activate(): void {
33    this.active = true;
34    this.voltage = this.advancedOptions?.startingVoltage || 10000; // Using optional property
35    console.log(`System fence ${this.id} activated. Voltage: ${this.voltage}V`);
36  }
37
38  deactivate(): void {
39    this.active = false;
40    this.voltage = 0;
41    console.log(`Fence system ${this.id} deactivated.`);
42  }
43
44  checkStatus(): string {
45    return `System fence ${this.id}: ${this.active ? "ACTIVE" : "INACTIVE"}, Voltage: ${this.voltage}V`;
46  }
47
48  // Custom method, not required by the interface
49  increaseVoltage(increment: number): void {
50    if (!this.active) {
51      console.log("Cannot change voltage - system inactive.");
52      return;
53    }
54
55    this.voltage += increment;
56    console.log(`Increased voltage to ${this.voltage}V.`);
57  }
58}
59
60// Usage with optional properties
61const tRexFence = new FenceSystem("FNC-TR1", {
62  startingVoltage: 20000,
63  automaticRestarts: true
64});
65
66const standardFence = new FenceSystem("FNC-ST1");
67
68tRexFence.activate();   // Uses custom starting voltage
69standardFence.activate();  // Uses default starting voltage
70
71// Error: Cannot assign to 'systemType' because it is a read-only property
72// tRexFence.systemType = "alarm";

Dans cet exemple :

  1. Nous avons défini une propriété readonly qui ne peut pas être modifiée après l'initialisation
  2. Nous avons ajouté une propriété optionnelle advancedOptions (marquée d'un ?)
  3. L'implémentation vérifie si la propriété optionnelle existe avant de l'utiliser

Étendre des interfaces

Les interfaces, comme les classes, peuvent étendre d'autres interfaces, en héritant de leurs propriétés et méthodes :

1// Basic interface for all animals
2interface Animal {
3  id: string;
4  species: string;
5  weight: number;
6  karma(): void;
7  monitor(): string;
8}
9
10// Extended interface specific to dinosaurs
11interface Dinosaur extends Animal {
12  dangerLevel: number;
13  dinosaurEra: "trias" | "jura" | "cretaceous";
14  getSpecialDiet(): string;
15}
16
17// Even more specialized interface
18interface PredatoryDinosaur extends Dinosaur {
19  biteForce: number;
20  speed: number;
21  hunt(): void;
22}
23
24// Implementation
25class TyrannosaurusRex implements PredatoryDinosaur {
26  // Properties from Animal interface
27  id: string;
28  species: string = "Tyrannosaurus Rex";
29  weight: number;
30
31  // Properties from Dinosaur interface
32  dangerLevel: number = 10;
33  dinosaurEra: "trias" | "jura" | "cretaceous" = "cretaceous";
34
35  // Properties from PredatoryDinosaur interface
36  biteForce: number;
37  speed: number;
38
39  // Custom properties
40  private age: number;
41  private lastFeeding: Date | null = null;
42
43  constructor(id: string, weight: number, age: number, biteForce: number, speed: number) {
44    this.id = id;
45    this.weight = weight;
46    this.age = age;
47    this.biteForce = biteForce;
48    this.speed = speed;
49  }
50
51  // Implementation of methods from Animal interface
52  karma(): void {
53    this.lastFeeding = new Date();
54    console.log(`T-Rex ${this.id} was fed.`);
55  }
56
57  monitor(): string {
58    const feedingStatus = this.lastFeeding
59      ? `Last feeding: ${this.lastFeeding.toLocaleString()}`
60      : "No feeding information";
61
62    return `T-Rex ${this.id}: Weight: ${this.weight}kg, Age: ${this.age} years. ${feedingStatus}`;
63  }
64
65  // Implementation of method from Dinosaur interface
66  getSpecialDiet(): string {
67    return "Fresh mammal meat, preferably beef, 250kg daily.";
68  }
69
70  // Implementation of method from PredatoryDinosaur interface
71  hunt(): void {
72    console.log(`T-Rex ${this.id} hunts at speed ${this.speed} km/h and bite force ${this.biteForce} PSI!`);
73  }
74}
75
76// Usage
77const rex = new TyrannosaurusRex("TREX-01", 8000, 12, 12800, 30);
78rex.karma();
79console.log(rex.monitor());
80console.log(`Special diet: ${rex.getSpecialDiet()}`);
81rex.hunt();

Dans cet exemple, nous avons créé une hiérarchie d'interfaces :

  1. Animal - l'interface de base pour tous les animaux
  2. Dinosaur - étend Animal avec des propriétés propres aux dinosaures
  3. PredatoryDinosaur - étend Dinosaur avec des propriétés propres aux prédateurs

La classe TyrannosaurusRex implémente l'interface la plus spécialisée PredatoryDinosaur, ce qui signifie qu'elle doit fournir toutes les propriétés et méthodes des trois interfaces.

Interfaces vs types

TypeScript propose aussi les alias de type (type), qui ont des capacités similaires aux interfaces, mais il existe quelques différences entre eux :

1// Interface
2interface EnclosureMeasurement {
3  width: number;
4  length: number;
5  calculateArea(): number;
6}
7
8// Type (Type alias)
9type CageMeasurement = {
10  width: number;
11  height: number;
12  depth: number;
13  calculateVolume(): number;
14};
15
16// Extending an interface
17interface EnclosureMeasurementWithFence extends EnclosureMeasurement {
18  fenceHeight: number;
19  fenceMaterial: string;
20}
21
22// "Extending" a type through intersection
23type ReinforcedCageMeasurement = CageMeasurement & {
24  reinforcementMaterial: string;
25  maxLoad: number;
26};
27
28// Implementation of interface
29class RaptorEnclosure implements EnclosureMeasurementWithFence {
30  width: number;
31  length: number;
32  fenceHeight: number;
33  fenceMaterial: string;
34
35  constructor(width: number, length: number, fenceHeight: number) {
36    this.width = width;
37    this.length = length;
38    this.fenceHeight = fenceHeight;
39    this.fenceMaterial = "Reinforced steel";
40  }
41
42  calculateArea(): number {
43    return this.width * this.length;
44  }
45}
46
47// Implementation of type (works the same as interface)
48class TransportCage implements ReinforcedCageMeasurement {
49  width: number;
50  height: number;
51  depth: number;
52  reinforcementMaterial: string;
53  maxLoad: number;
54
55  constructor(
56    width: number,
57    height: number,
58    depth: number,
59    maxLoad: number
60  ) {
61    this.width = width;
62    this.height = height;
63    this.depth = depth;
64    this.reinforcementMaterial = "Carbon composite";
65    this.maxLoad = maxLoad;
66  }
67
68  calculateVolume(): number {
69    return this.width * this.height * this.depth;
70  }
71}

Principales différences entre interfaces et types :

  1. Extensibilité :

    • Les interfaces peuvent être étendues ultérieurement (fusion de déclarations)
    • Les types sont fermés après leur définition
    1// Interface can be extended with additional declarations
    2interface DinosaurDiagnosis {
    3  examinationResult: string;
    4}
    5
    6// Adding new properties to an existing interface
    7interface DinosaurDiagnosis {
    8  executionDate: Date;
    9}
    10
    11// Now DinosaurDiagnosis has both properties!
    12
    13// For types this won't work:
    14type EnclosureDescription = {
    15  id: string;
    16};
    17
    18// Error: Duplicate identifier 'EnclosureDescription'
    19// type EnclosureDescription = {
    20//   name: string;
    21// };
  2. Combinaison :

    • Les interfaces utilisent extends
    • Les types utilisent l'opérateur d'intersection &
  3. Constructions avancées :

    • Les types peuvent représenter des unions, des intersections et d'autres types complexes
    • Les interfaces se limitent à décrire la forme des objets
1// Advanced types that cannot be expressed with interfaces
2type DinosaurStatus = "healthy" | "sick" | "in_treatment" | "quarantined";
3
4type DinosaurID = string | number;
5
6type AnimalOrPlant = Animal | { genus: string, height: number };
7
8// Mapped types
9type Optional<T> = {
10  [P in keyof T]?: T[P];
11};
12
13// Using a mapped type with an interface
14type OptionalDinosaur = Optional<Dinosaur>;

Interfaces pour les fonctions et les méthodes

Les interfaces peuvent aussi définir la forme des fonctions et des constructeurs de classe :

1// Interface for a function
2interface PhotoProcessing {
3  (path: string, options?: { resolution?: number, filter?: string }): Promise<string>;
4}
5
6// Implementation of a function conforming to the interface
7const dinosaurPhotoProcessor: PhotoProcessing = async (path, options) => {
8  console.log(`Processing photo: ${path}`);
9  console.log(`Options: ${JSON.stringify(options || {})}`);
10
11  // Simulation of processing
12  await new Promise(resolve => setTimeout(resolve, 1000));
13
14  return `optimized_${path}`;
15};
16
17// Calling the function
18dinosaurPhotoProcessor("trex.jpg", { resolution: 1200 })
19  .then(result => console.log(`Processed: ${result}`));
20
21// Interface for a class constructor
22interface DinosaurConstructor {
23  new (id: string, species: string, age: number): Animal;
24}
25
26// Function using a constructor
27function createDinosaur(Constructor: DinosaurConstructor, id: string, species: string, age: number): Animal {
28  return new Constructor(id, species, age);
29}
30
31// Class conforming to the constructor interface
32class DinosaurPrototype implements Animal {
33  id: string;
34  species: string;
35  weight: number = 0;
36  private _age: number;
37
38  constructor(id: string, species: string, age: number) {
39    this.id = id;
40    this.species = species;
41    this._age = age;
42
43    // Weight calculation based on age
44    this.weight = age * 100; // Simplified calculation
45  }
46
47  karma(): void {
48    console.log(`Feeding prototype ${this.species}.`);
49  }
50
51  monitor(): string {
52    return `Monitoring prototype ${this.species} (ID: ${this.id}).`;
53  }
54}
55
56// Using the factory function
57const newDinosaur = createDinosaur(DinosaurPrototype, "PROTO-1", "Testosaurus", 5);
58console.log(newDinosaur.monitor());

Signatures d'index dans les interfaces

Les interfaces peuvent aussi définir des types indexés qui permettent un accès dynamique aux propriétés :

1// Interface with index signatures
2interface AnimalDatabase {
3  // String index, values of type Animal
4  [id: string]: Animal;
5
6  // We can also add specific properties
7  animalCount: number;
8  lastUpdate: Date;
9}
10
11// Implementation
12class AnimalRepository implements AnimalDatabase {
13  // Implementation of index
14  [id: string]: Animal | number | Date | Function;
15
16  animalCount: number = 0;
17  lastUpdate: Date = new Date();
18
19  // We need to handle methods, because they are also properties
20  addAnimal(animal: Animal): void {
21    this[animal.id] = animal;
22    this.animalCount++;
23    this.lastUpdate = new Date();
24  }
25
26  getAnimal(id: string): Animal | undefined {
27    const animal = this[id];
28    if (animal && typeof animal !== 'number' && !(animal instanceof Date) && typeof animal !== 'function') {
29      return animal;
30    }
31    return undefined;
32  }
33}
34
35// Usage
36const animalRepo = new AnimalRepository();
37
38// Adding some animals
39const triceratops = {
40  id: "TRIC-01",
41  species: "Triceratops",
42  weight: 6000,
43  karma(): void { console.log("Feeding Triceratops"); },
44  monitor(): string { return "Monitoring Triceratops"; }
45};
46
47const stegosaurus = {
48  id: "STEG-01",
49  species: "Stegosaurus",
50  weight: 5000,
51  karma(): void { console.log("Feeding Stegosaurus"); },
52  monitor(): string { return "Monitoring Stegosaurus"; }
53};
54
55animalRepo.addAnimal(triceratops);
56animalRepo.addAnimal(stegosaurus);
57
58// Access through indexes
59const animal = animalRepo.getAnimal("TRIC-01");
60if (animal) {
61  console.log(`Found: ${animal.species}`);
62  animal.karma();
63}
64
65console.log(`Number of animals in repository: ${animalRepo.animalCount}`);
66console.log(`Last update: ${animalRepo.lastUpdate.toLocaleString()}`);

Implémenter plusieurs interfaces

Une classe peut implémenter plusieurs interfaces en même temps :

1// Interface for monitoring capability
2interface Monitorable {
3  startMonitoring(): void;
4  stopMonitoring(): void;
5  getMonitoringStatus(): string;
6}
7
8// Interface for alarm systems
9interface AlarmSystem {
10  startAlarm(): void;
11  disableAlarm(): void;
12  isAlarmActive: boolean;
13}
14
15// Interface for access control systems
16interface AccessControl {
17  checkAccess(permissionLevel: number): boolean;
18  blockAccess(): void;
19  unblockAccess(): void;
20}
21
22// Class implementing all three interfaces
23class AdvancedSecuritySystem implements Monitorable, AlarmSystem, AccessControl {
24  private id: string;
25  private activeMonitoring: boolean = false;
26  isAlarmActive: boolean = false;
27  private accessBlocked: boolean = false;
28  private minimumPermissionLevel: number;
29
30  constructor(id: string, minimumPermissionLevel: number = 3) {
31    this.id = id;
32    this.minimumPermissionLevel = minimumPermissionLevel;
33  }
34
35  // Implementation of Monitorable
36  startMonitoring(): void {
37    this.activeMonitoring = true;
38    console.log(`System ${this.id}: Monitoring started.`);
39  }
40
41  stopMonitoring(): void {
42    this.activeMonitoring = false;
43    console.log(`System ${this.id}: Monitoring stopped.`);
44  }
45
46  getMonitoringStatus(): string {
47    return `System ${this.id}: Monitoring ${this.activeMonitoring ? "active" : "inactive"}`;
48  }
49
50  // Implementation of AlarmSystem
51  startAlarm(): void {
52    this.isAlarmActive = true;
53    this.blockAccess(); // Automatic access block
54    console.log(`System ${this.id}: !!! ALARM STARTED !!!`);
55  }
56
57  disableAlarm(): void {
58    this.isAlarmActive = false;
59    console.log(`System ${this.id}: Alarm disabled.`);
60  }
61
62  // Implementation of AccessControl
63  checkAccess(permissionLevel: number): boolean {
64    if (this.accessBlocked) {
65      console.log(`System ${this.id}: Access blocked for all permission levels.`);
66      return false;
67    }
68
69    const accessGranted = permissionLevel >= this.minimumPermissionLevel;
70    console.log(`System ${this.id}: Checking access for level ${permissionLevel}: ${accessGranted ? "GRANTED" : "DENIED"}`);
71    return accessGranted;
72  }
73
74  blockAccess(): void {
75    this.accessBlocked = true;
76    console.log(`System ${this.id}: Access blocked.`);
77  }
78
79  unblockAccess(): void {
80    if (this.isAlarmActive) {
81      console.log(`System ${this.id}: Cannot unblock access - alarm active!`);
82      return;
83    }
84
85    this.accessBlocked = false;
86    console.log(`System ${this.id}: Access unblocked.`);
87  }
88
89  // Custom method combining functionalities from different interfaces
90  respondToBreakIn(): void {
91    console.log(`System ${this.id}: Break-in attempt detected!`);
92    this.startMonitoring();
93    this.startAlarm();
94    // Access is already blocked by startAlarm()
95  }
96}
97
98// Using a class implementing multiple interfaces
99const labSecuritySystem = new AdvancedSecuritySystem("SEC-LAB-01", 4);
100
101// We can use methods from different interfaces
102labSecuritySystem.startMonitoring();
103console.log(labSecuritySystem.getMonitoringStatus());
104
105// Checking access
106console.log(labSecuritySystem.checkAccess(3)); // Denied - required level 4
107console.log(labSecuritySystem.checkAccess(5)); // Access granted
108
109// Reaction to break-in
110labSecuritySystem.respondToBreakIn();
111
112// Attempting to unblock access during alarm
113labSecuritySystem.unblockAccess(); // Will not allow unlock
114
115// Disabling alarm and unblocking
116labSecuritySystem.disableAlarm();
117labSecuritySystem.unblockAccess();

Implémenter des interfaces avec des conflits de noms

Que se passe-t-il lorsqu'on implémente deux interfaces ayant des propriétés du même nom ?

1// Two interfaces with properties of the same name
2interface SystemA {
3  id: string;
4  status: string;
5  activate(): void;
6}
7
8interface SystemB {
9  id: number; // Type conflict - string vs number
10  status: string; // Same type, no conflict
11  activate(code: string): boolean; // Signature conflict
12}
13
14// Attempting to implement both interfaces
15// This implementation will cause a compilation error
16/*
17class ProblematicSituation implements SystemA, SystemB {
18  id: string; // Cannot be both string and number
19  status: string; // OK, same type in both interfaces
20
21  // Attempting to implement both activate signatures
22  activate(code?: string): boolean | void {
23    if (code) {
24      console.log(`Activation with code: ${code}`);
25      return true;
26    } else {
27      console.log("Activation without code");
28    }
29  }
30}
31*/
32
33// Correct implementation:
34// We need to create a type compatible with both interfaces
35interface CombinedSystem extends SystemA, SystemB {
36  // Resolving the id conflict
37  id: string & number; // Must be both string and number (impossible in practice)
38
39  // Status without conflict
40  status: string;
41
42  // Resolving the method conflict
43  activate(code?: string): boolean | void;
44}
45
46// In practice, it's often impossible to implement both interfaces simultaneously
47// In such cases, it's better to modify one of the interfaces or
48// create an adapter that delegates calls

Comment résoudre le problème des interfaces incompatibles :

1// Better approach: Using composition instead of inheritance
2class AccessSystem {
3  private systemA: SystemAImplementation;
4  private systemB: SystemBImplementation;
5
6  constructor() {
7    this.systemA = new SystemAImplementation("SYS-A");
8    this.systemB = new SystemBImplementation(1001);
9  }
10
11  // Methods delegating to appropriate implementations
12  activateA(): void {
13    this.systemA.activate();
14  }
15
16  activateB(code: string): boolean {
17    return this.systemB.activate(code);
18  }
19
20  getStatusA(): string {
21    return this.systemA.status;
22  }
23
24  getStatusB(): string {
25    return this.systemB.status;
26  }
27}
28
29// Single interface implementations
30class SystemAImplementation implements SystemA {
31  id: string;
32  status: string = "inactive";
33
34  constructor(id: string) {
35    this.id = id;
36  }
37
38  activate(): void {
39    this.status = "active";
40    console.log(`System A (${this.id}) activated`);
41  }
42}
43
44class SystemBImplementation implements SystemB {
45  id: number;
46  status: string = "offline";
47
48  constructor(id: number) {
49    this.id = id;
50  }
51
52  activate(code: string): boolean {
53    if (code === "secret-password") {
54      this.status = "online";
55      console.log(`System B (${this.id}) activated with code`);
56      return true;
57    }
58    console.log(`System B (${this.id}) - incorrect activation code`);
59    return false;
60  }
61}

Exemple pratique : système de gestion de Jurassic Park

Voici un exemple complet utilisant des interfaces pour créer un système de gestion de Jurassic Park :

1// Interface for a basic manageable entity
2interface InventoryItem {
3  readonly id: string;
4  name: string;
5  category: string;
6  additionDate: Date;
7  lastUpdate: Date;
8  status: "active" | "inactive" | "under_repair" | "retired";
9
10  updateStatus(newStatus: "active" | "inactive" | "under_repair" | "retired"): void;
11  getInfo(): Record<string, any>;
12}
13
14// Interface for trackable objects
15interface Tracked {
16  currentLocation: { x: number, y: number } | null;
17  locationHistory: Array<{ time: Date, x: number, y: number }>;
18
19  updateLocation(x: number, y: number): void;
20  getLastLocation(): { time: Date, x: number, y: number } | null;
21  getMovementRoute(fromTime: Date, toTime?: Date): Array<{ time: Date, x: number, y: number }>;
22}
23
24// Interface for monitorable objects
25interface Monitored {
26  monitoredParameters: Map<string, number | string | boolean>;
27  measurementHistory: Array<{ time: Date, parameter: string, value: number | string | boolean }>;
28  monitoringStatus: "enabled" | "disabled" | "limited";
29
30  startMonitoring(): void;
31  stopMonitoring(): void;
32  addMeasurement(parameter: string, value: number | string | boolean): void;
33  getRecentMeasurements(count?: number): Array<{ time: Date, parameter: string, value: number | string | boolean }>;
34}
35
36// Interface for objects requiring regular maintenance
37interface Maintainable {
38  requiredMaintenanceLevel: number; // 1-5
39  lastMaintenance: Date | null;
40  plannedMaintenance: Date | null;
41  maintenanceHistory: Array<{ data: Date, description: string, contractor: string }>;
42
43  scheduleMaintenance(data: Date): void;
44  performMaintenance(description: string, contractor: string): void;
45  checkMaintenanceStatus(): "required" | "scheduled" | "current";
46}
47
48// Interface for security systems
49interface SecuritySystem {
50  threatLevel: 1 | 2 | 3 | 4 | 5;
51  securityZone: string;
52  authorizedPersons: string[];
53
54  raiseThreatLevel(): void;
55  lowerThreatLevel(): void;
56  checkPermissions(employeeId: string): boolean;
57  respondToThreat(description: string): void;
58}
59
60// Implementation of a dinosaur tracking device
61class TrackingDeviceT3000 implements InventoryItem, Tracked, Monitored, Maintainable {
62  // InventoryItem (Inventory Item)
63  readonly id: string;
64  name: string;
65  category: string = "Tracking Device";
66  additionDate: Date;
67  lastUpdate: Date;
68  status: "active" | "inactive" | "under_repair" | "retired";
69
70  // Tracked (Tracked)
71  currentLocation: { x: number, y: number } | null = null;
72  locationHistory: Array<{ time: Date, x: number, y: number }> = [];
73
74  // Monitored (Monitored)
75  monitoredParameters: Map<string, number | string | boolean> = new Map();
76  measurementHistory: Array<{ time: Date, parameter: string, value: number | string | boolean }> = [];
77  monitoringStatus: "enabled" | "disabled" | "limited" = "disabled";
78
79  // Maintainable (Maintainable)
80  requiredMaintenanceLevel: number = 3;
81  lastMaintenance: Date | null = null;
82  plannedMaintenance: Date | null = null;
83  maintenanceHistory: Array<{ data: Date, description: string, contractor: string }> = [];
84
85  // Custom properties
86  private batteryLevel: number = 100; // in percent
87  private assignedDinosaur: string | null = null;
88
89  constructor(id: string, name: string) {
90    this.id = id;
91    this.name = name;
92    this.additionDate = new Date();
93    this.lastUpdate = new Date();
94    this.status = "inactive";
95
96    // Initializing monitoring parameters
97    this.monitoredParameters.set("battery", this.batteryLevel);
98    this.monitoredParameters.set("active", false);
99    this.monitoredParameters.set("temperature", 25.0);
100  }
101
102  // Implementation of InventoryItem methods
103  updateStatus(newStatus: "active" | "inactive" | "under_repair" | "retired"): void {
104    this.status = newStatus;
105    this.lastUpdate = new Date();
106    console.log(`Device ${this.id}: Changed status to ${newStatus}`);
107  }
108
109  getInfo(): Record<string, any> {
110    return {
111      id: this.id,
112      name: this.name,
113      category: this.category,
114      status: this.status,
115      additionDate: this.additionDate,
116      lastUpdate: this.lastUpdate,
117      batteryLevel: this.batteryLevel,
118      assignedDinosaur: this.assignedDinosaur
119    };
120  }
121
122  // Implementation of Tracked methods
123  updateLocation(x: number, y: number): void {
124    const time = new Date();
125
126    // Updating current location
127    this.currentLocation = { x, y };
128
129    // Adding to history
130    this.locationHistory.push({ time, x, y });
131
132    // Simulating battery drain
133    this.reduceBattery(0.1);
134
135    console.log(`Device ${this.id}: Updated location to [x: ${x}, y: ${y}]`);
136  }
137
138  getLastLocation(): { time: Date, x: number, y: number } | null {
139    if (this.locationHistory.length === 0) {
140      return null;
141    }
142
143    return this.locationHistory[this.locationHistory.length - 1];
144  }
145
146  getMovementRoute(fromTime: Date, toTime: Date = new Date()): Array<{ time: Date, x: number, y: number }> {
147    return this.locationHistory.filter(loc =>
148      loc.time >= fromTime && loc.time <= toTime
149    );
150  }
151
152  // Implementation of Monitored methods
153  startMonitoring(): void {
154    if (this.monitoringStatus !== "enabled") {
155      this.monitoringStatus = "enabled";
156      console.log(`Device ${this.id}: Monitoring started`);
157
158      // Simulating measurement when monitoring is enabled
159      this.addMeasurement("active", true);
160    }
161  }
162
163  stopMonitoring(): void {
164    if (this.monitoringStatus !== "disabled") {
165      this.monitoringStatus = "disabled";
166      console.log(`Device ${this.id}: Monitoring stopped`);
167
168      // Updating the activity parameter
169      this.monitoredParameters.set("active", false);
170      this.addMeasurement("active", false);
171    }
172  }
173
174  addMeasurement(parameter: string, value: number | string | boolean): void {
175    const time = new Date();
176
177    // Updating the current parameter value
178    this.monitoredParameters.set(parameter, value);
179
180    // Adding to measurement history
181    this.measurementHistory.push({ time, parameter, value });
182
183    // Updating the last update time
184    this.lastUpdate = time;
185
186    console.log(`Device ${this.id}: Added pomiar ${parameter} = ${value}`);
187  }
188
189  getRecentMeasurements(count: number = 10): Array<{ time: Date, parameter: string, value: number | string | boolean }> {
190    // Returns the specified number of most recent measurements
191    return this.measurementHistory.slice(-count);
192  }
193
194  // Implementation of Maintainable methods
195  scheduleMaintenance(data: Date): void {
196    this.plannedMaintenance = data;
197    console.log(`Device ${this.id}: Scheduled maintenance for ${data.toLocaleDateString()}`);
198  }
199
200  performMaintenance(description: string, contractor: string): void {
201    const date = new Date();
202
203    // Adding to maintenance history
204    this.maintenanceHistory.push({ date, description, contractor });
205
206    // Updating the last maintenance date
207    this.lastMaintenance = date;
208
209    // Resetting planned maintenance
210    this.plannedMaintenance = null;
211
212    // Updating status and battery after maintenance
213    this.updateStatus("active");
214    this.batteryLevel = 100;
215    this.monitoredParameters.set("battery", this.batteryLevel);
216    this.addMeasurement("battery", this.batteryLevel);
217
218    console.log(`Device ${this.id}: Maintenance performed by ${contractor}: ${description}`);
219  }
220
221  checkMaintenanceStatus(): "required" | "scheduled" | "current" {
222    // No previous maintenance - required
223    if (!this.lastMaintenance) {
224      return "required";
225    }
226
227    // Planned maintenance in the future
228    if (this.plannedMaintenance && this.plannedMaintenance > new Date()) {
229      return "scheduled";
230    }
231
232    // Checking if more than 90 days have passed since the last maintenance
233    const today = new Date();
234    const differenceMilliseconds = today.getTime() - this.lastMaintenance.getTime();
235    const differenceDays = differenceMilliseconds / (1000 * 60 * 60 * 24);
236
237    if (differenceDays > 90 || this.batteryLevel < 20) {
238      return "required";
239    }
240
241    return "current";
242  }
243
244  // Custom methods
245  assignDinosaur(dinosaurId: string): void {
246    this.assignedDinosaur = dinosaurId;
247    this.addMeasurement("assignedDinosaur", dinosaurId);
248    console.log(`Device ${this.id}: Assigned to dinosaur ${dinosaurId}`);
249  }
250
251  private reduceBattery(value: number): void {
252    this.batteryLevel = Math.max(0, this.batteryLevel - value);
253    this.monitoredParameters.set("battery", this.batteryLevel);
254
255    // If battery is critically low - change status
256    if (this.batteryLevel < 5 && this.status === "active") {
257      this.updateStatus("inactive");
258      console.log(`Device ${this.id}: Critical battery level! Device disabled.`);
259    }
260  }
261}
262
263// Implementation of dinosaur enclosure security system
264class ElectricFence implements InventoryItem, SecuritySystem, Maintainable {
265  // InventoryItem (Inventory Item)
266  readonly id: string;
267  name: string;
268  category: string = "Fence Electric";
269  additionDate: Date;
270  lastUpdate: Date;
271  status: "active" | "inactive" | "under_repair" | "retired";
272
273  // SecuritySystem (Security System)
274  threatLevel: 1 | 2 | 3 | 4 | 5 = 1;
275  securityZone: string;
276  authorizedPersons: string[] = [];
277
278  // Maintainable (Maintainable)
279  requiredMaintenanceLevel: number = 5; // Highest level - critical for security
280  lastMaintenance: Date | null = null;
281  plannedMaintenance: Date | null = null;
282  maintenanceHistory: Array<{ data: Date, description: string, contractor: string }> = [];
283
284  // Custom properties
285  private voltage: number; // in volts
286  private fenceLength: number; // in meters
287  private motionSensors: boolean;
288
289  constructor(
290    id: string,
291    name: string,
292    securityZone: string,
293    fenceLength: number,
294    voltage: number = 10000,
295    motionSensors: boolean = true
296  ) {
297    this.id = id;
298    this.name = name;
299    this.securityZone = securityZone;
300    this.fenceLength = fenceLength;
301    this.voltage = voltage;
302    this.motionSensors = motionSensors;
303
304    this.additionDate = new Date();
305    this.lastUpdate = new Date();
306    this.status = "inactive";
307
308    // Adding system administrators as authorized personnel
309    this.authorizedPersons.push("ADMIN-001", "SEC-CHIEF", "TECH-HEAD");
310  }
311
312  // Implementation of InventoryItem
313  updateStatus(newStatus: "active" | "inactive" | "under_repair" | "retired"): void {
314    // If switching from inactive to active
315    if (this.status !== "active" && newStatus === "active") {
316      // Check if maintenance is required
317      if (this.checkMaintenanceStatus() === "required") {
318        console.log(`Fence ${this.id}: WARNING - Activation without required maintenance!`);
319      }
320    }
321
322    this.status = newStatus;
323    this.lastUpdate = new Date();
324
325    // Automatically adjust voltage based on status
326    if (newStatus === "active") {
327      console.log(`Fence ${this.id}: Activated with voltage ${this.voltage}V`);
328    } else if (newStatus === "inactive" || newStatus === "under_repair") {
329      this.voltage = 0;
330      console.log(`Fence ${this.id}: Deactivated - voltage 0V`);
331    }
332  }
333
334  getInfo(): Record<string, any> {
335    return {
336      id: this.id,
337      name: this.name,
338      category: this.category,
339      status: this.status,
340      securityZone: this.securityZone,
341      fenceLength: this.fenceLength,
342      voltage: this.voltage,
343      motionSensors: this.motionSensors,
344      threatLevel: this.threatLevel
345    };
346  }
347
348  // Implementation of SecuritySystem
349  raiseThreatLevel(): void {
350    if (this.threatLevel < 5) {
351      this.threatLevel = (this.threatLevel + 1) as 1 | 2 | 3 | 4 | 5;
352
353      // Automatic voltage increase
354      this.increaseVoltage(2000 * this.threatLevel);
355
356      console.log(`Fence ${this.id}: Raised threat level to ${this.threatLevel}. New voltage: ${this.voltage}V`);
357    } else {
358      console.log(`Fence ${this.id}: Threat level already at maximum level 5`);
359    }
360  }
361
362  lowerThreatLevel(): void {
363    if (this.threatLevel > 1) {
364      this.threatLevel = (this.threatLevel - 1) as 1 | 2 | 3 | 4 | 5;
365
366      // Automatic voltage decrease (but still active)
367      const newVoltage = 10000 + (2000 * (this.threatLevel - 1));
368      this.voltage = newVoltage;
369
370      console.log(`Fence ${this.id}: Lowered threat level to ${this.threatLevel}. New voltage: ${this.voltage}V`);
371    } else {
372      console.log(`Fence ${this.id}: Threat level already at minimum level 1`);
373    }
374  }
375
376  checkPermissions(employeeId: string): boolean {
377    return this.authorizedPersons.includes(employeeId);
378  }
379
380  respondToThreat(description: string): void {
381    console.log(`Fence ${this.id}: ALERT! ${description}`);
382
383    // Maximum threat level
384    this.threatLevel = 5;
385
386    // Maximum voltage
387    this.voltage = 30000;
388
389    console.log(`Fence ${this.id}: Threat level 5. Maximum voltage: ${this.voltage}V`);
390  }
391
392  // Implementation of Maintainable
393  scheduleMaintenance(data: Date): void {
394    this.plannedMaintenance = data;
395    console.log(`Fence ${this.id}: Scheduled maintenance for ${date.toLocaleDateString()}`);
396  }
397
398  performMaintenance(description: string, contractor: string): void {
399    const date = new Date();
400
401    // Safety measure - temporary fence deactivation during maintenance
402    const previousStatus = this.status;
403    this.updateStatus("under_repair");
404
405    // Add to maintenance history
406    this.maintenanceHistory.push({ date, description, contractor });
407
408    // Update last maintenance date
409    this.lastMaintenance = date;
410
411    // Reset planned maintenance
412    this.plannedMaintenance = null;
413
414    console.log(`Fence ${this.id}: Maintenance performed by ${contractor}: ${description}`);
415
416    // Restoring previous status
417    this.updateStatus(previousStatus);
418  }
419
420  checkMaintenanceStatus(): "required" | "scheduled" | "current" {
421    // No prior maintenance - required
422    if (!this.lastMaintenance) {
423      return "required";
424    }
425
426    // Maintenance scheduled in the future
427    if (this.plannedMaintenance && this.plannedMaintenance > new Date()) {
428      return "scheduled";
429    }
430
431    // Checking if more than 30 days have passed since last maintenance (shorter period for fences)
432    const today = new Date();
433    const differenceMilliseconds = today.getTime() - this.lastMaintenance.getTime();
434    const differenceDays = differenceMilliseconds / (1000 * 60 * 60 * 24);
435
436    if (differenceDays > 30) {
437      return "required";
438    }
439
440    return "current";
441  }
442
443  // Custom methods
444  addAuthorizedPerson(employeeId: string): void {
445    if (!this.authorizedPersons.includes(employeeId)) {
446      this.authorizedPersons.push(employeeId);
447      console.log(`Enclosure ${this.id}: Added permissions for employee ${employeeId}`);
448    }
449  }
450
451  removeAuthorizedPerson(employeeId: string): void {
452    const index = this.authorizedPersons.indexOf(employeeId);
453    if (index !== -1) {
454      this.authorizedPersons.splice(index, 1);
455      console.log(`Fence ${this.id}: Removed permissions for employee ${employeeId}`);
456    }
457  }
458
459  increaseVoltage(value: number): void {
460    if (this.status !== "active") {
461      console.log(`Fence ${this.id}: Cannot change voltage - fence is not active`);
462      return;
463    }
464
465    this.voltage += value;
466    console.log(`Fence ${this.id}: Increased voltage to ${this.voltage}V`);
467  }
468
469  decreaseVoltage(value: number): void {
470    if (this.status !== "active") {
471      console.log(`Fence ${this.id}: Cannot change voltage - fence is not active`);
472      return;
473    }
474
475    this.voltage = Math.max(0, this.voltage - value);
476    console.log(`Fence ${this.id}: Decreased voltage to ${this.voltage}V`);
477  }
478}
479
480// Demonstrating the use of our implementations
481function demonstrateParkSystem() {
482  console.log("=== JURASSIC PARK MANAGEMENT SYSTEM DEMONSTRATION ===");
483
484  // Creating a tracking device
485  console.log("\n--- Initializing tracking device ---");
486  const trackingDevice = new TrackingDeviceT3000("TRACK-001", "T-Rex Tracker #1");
487  trackingDevice.updateStatus("active");
488  trackingDevice.startMonitoring();
489  trackingDevice.assignDinosaur("TREX-001");
490
491  // Simulating location tracking
492  trackingDevice.updateLocation(135.42, 86.21);
493  trackingDevice.updateLocation(135.50, 86.35);
494  trackingDevice.updateLocation(135.62, 86.40);
495
496  // Creating a fence
497  console.log("\n--- Fence initialization ---");
498  const fence = new ElectricFence(
499    "FENCE-002",
500    "Fence enclosure T-Rex",
501    "T-REX-A",
502    1200, // 1200 meters long
503    15000, // 15000V
504    true // from motion sensors
505  );
506
507  // Adding permissions for an employee
508  fence.addAuthorizedPerson("EMP-047"); // Owen Grady
509
510  // Activating the fence
511  fence.updateStatus("active");
512
513  // Performing device maintenance
514  console.log("\n--- Device maintenance ---");
515  trackingDevice.scheduleMaintenance(new Date("2023-06-15"));
516  trackingDevice.performMaintenance("Battery replacement and GPS calibration", "Tech Ivan");
517
518  // Simulating an alarm
519  console.log("\n--- Simulation incident ---");
520  console.log("T-Rex is trying to break through the fence!");
521
522  // Tracking device registers rapid movement
523  trackingDevice.updateLocation(135.90, 86.70);
524  trackingDevice.updateLocation(136.10, 86.90);
525  trackingDevice.addMeasurement("acceleration", 12.5);
526
527  // Fence responds to threat
528  fence.respondToThreat("T-Rex escape attempt detected");
529
530  // Status checks after incident
531  console.log("\n--- Post-incident report ---");
532  console.log("Last dinosaur location:", trackingDevice.getLastLocation());
533  console.log("Status fence:", fence.getInfo());
534
535  // Returning to normall mode
536  console.log("\n--- Normalization situation ---");
537  fence.lowerThreatLevel();
538  fence.lowerThreatLevel();
539
540  console.log("\n=== END DEMONSTRATION ===");
541}
542
543// Running the demonstration
544demonstrateParkSystem();

Résumé

Dans cet exercice, nous avons découvert les interfaces TypeScript - un outil puissant pour définir des contrats et créer des structures cohérentes dans le code.

Caractéristiques clés des interfaces :

  1. Définir la forme des objets - préciser quelles propriétés et méthodes les objets d'un type donné doivent avoir
  2. Prise en charge du polymorphisme - différentes classes peuvent implémenter la même interface de différentes manières
  3. Couplage faible - se concentrer sur ce que font les objets, et non sur la façon dont ils sont implémentés
  4. Flexibilité - propriétés optionnelles, en lecture seule, signatures d'index, etc.
  5. Extensibilité - les interfaces peuvent hériter d'autres interfaces

Comme le résumerait Claire Dearing : « À Jurassic Park, comme dans une bonne architecture logicielle, chacun a son rôle et ses responsabilités. Les interfaces nous permettent de définir clairement ces rôles, en garantissant que tous les systèmes coopèrent bien - sans surprises. Et crois-moi, les surprises dans un parc rempli de dinosaures sont la dernière chose dont nous avons besoin. »

Code de cette leçon : index.ts
1// Implémentation des interfaces - Parc Jurassique
2console.log("=== IMPLÉMENTATION DES INTERFACES ===\n");
3
4// ===========================================
5// 1. Définition des interfaces
6// ===========================================
7
8interface ITrackable {
9  readonly trackingId: string;
10  location: { x: number; y: number };
11  getLocation(): string;
12  updateLocation(x: number, y: number): void;
13}
14
15interface IFeedable {
16  diet: "carnivore" | "herbivore" | "omnivore";
17  lastFedAt: Date;
18  feed(food: string): string;
19  isHungry(): boolean;
20}
21
22interface IContainable {
23  enclosureId: string;
24  isContained: boolean;
25  checkContainment(): string;
26}
27
28// ===========================================
29// 2. Classe implémentant plusieurs interfaces
30// ===========================================
31
32class ParkDinosaur implements ITrackable, IFeedable, IContainable {
33  readonly trackingId: string;
34  location: { x: number; y: number };
35  diet: "carnivore" | "herbivore" | "omnivore";
36  lastFedAt: Date;
37  enclosureId: string;
38  isContained: boolean;
39  name: string;
40
41  constructor(
42    name: string,
43    diet: "carnivore" | "herbivore" | "omnivore",
44    enclosureId: string
45  ) {
46    this.name = name;
47    this.trackingId = `TRK-${Math.random().toString(36).substring(2, 8).toUpperCase()}`;
48    this.location = { x: 0, y: 0 };
49    this.diet = diet;
50    this.lastFedAt = new Date();
51    this.enclosureId = enclosureId;
52    this.isContained = true;
53  }
54
55  // ITrackable
56  getLocation(): string {
57    return `(${this.location.x}, ${this.location.y})`;
58  }
59
60  updateLocation(x: number, y: number): void {
61    this.location = { x, y };
62    console.log(`  ${this.name} déplacé vers ${this.getLocation()}`);
63  }
64
65  // IFeedable
66  feed(food: string): string {
67    this.lastFedAt = new Date();
68    return `${this.name} a mangé: ${food}`;
69  }
70
71  isHungry(): boolean {
72    const hoursSinceFed = (Date.now() - this.lastFedAt.getTime()) / 3600000;
73    return hoursSinceFed > 4;
74  }
75
76  // IContainable
77  checkContainment(): string {
78    return this.isContained
79      ? `${this.name} est en sécurité dans l'enclos ${this.enclosureId}`
80      : `ALARM! ${this.name} s'est échappé de l'enclos ${this.enclosureId}!`;
81  }
82}
83
84// ===========================================
85// 3. Fonctions opérant sur les interfaces
86// ===========================================
87
88function trackEntity(entity: ITrackable): void {
89  console.log(`Suivi: ${entity.trackingId} à la position ${entity.getLocation()}`);
90}
91
92function feedEntity(entity: IFeedable): void {
93  console.log(`Diet: ${entity.diet}`);
94  console.log(`Affamé: ${entity.isHungry() ? "OUI" : "NON"}`);
95}
96
97function checkSafety(entities: IContainable[]): void {
98  console.log("\n--- Rapport de sécurité ---");
99  entities.forEach(e => console.log(e.checkContainment()));
100}
101
102// ===========================================
103// Test
104// ===========================================
105
106const velociraptor = new ParkDinosaur("Blue", "carnivore", "ENC-A7");
107const brachio = new ParkDinosaur("Patty", "herbivore", "ENC-B3");
108
109console.log("--- Suivi ---");
110trackEntity(velociraptor);
111velociraptor.updateLocation(45, 120);
112trackEntity(velociraptor);
113
114console.log("\n--- Alimentation ---");
115console.log(velociraptor.feed("Chèvre"));
116console.log(brachio.feed("Feuilles de palmier"));
117feedEntity(brachio);
118
119brachio.isContained = false; // Évasion !
120checkSafety([velociraptor, brachio]);
121
122console.log("\nInterfaces enable polymorphism and loose coupling!");

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 fait le mot-clé implements en TypeScript ?

  2. 2. Comment définit-on une interface générique en TypeScript ?

Tâches pratiques dans le jeu

  • Éditeur de code

    Implémente les interfaces ITrackable et IFeedable pour les entités du parc

  • Tri horizontal

    Range les éléments d'une interface générique :

Articles utiles