Cours JavaScript et TypeScript · Module 7: TypeScript avancé

Restriction de type et gardes de type

10 min de lecture
Dans cette leçon7

« À Jurassic Park, il ne suffit pas de savoir qu'on a un dinosaure devant soi », dit Robert Muldoon, le garde-chasse en chef du parc. « Il faut savoir exactement S'IL s'agit d'un prédateur, S'IL est herbivore, S'IL vole, S'IL nage. Le protocole de sécurité que tu déclenches en dépend. Une seule mauvaise supposition, et c'est la catastrophe. »

La restriction de type en TypeScript fonctionne de manière similaire - c'est le processus consistant à restreindre un type large à un type plus précis. Lorsqu'une variable a le type string | number, TypeScript ne sait pas s'il s'agit d'un texte ou d'un nombre. Grâce aux gardes de type, nous pouvons « restreindre » le type pour que le compilateur sache exactement à quoi il a affaire. C'est une technique fondamentale pour écrire du code TypeScript sûr.

L'opérateur typeof comme garde de type

Le garde de type le plus simple est typeof - un opérateur JavaScript que TypeScript comprend et utilise pour la restriction de type. Lorsque tu vérifies le type d'une variable avec typeof, le compilateur sait automatiquement quel type a la variable à l'intérieur du bloc if.

1// Function accepting different types of dinosaur data
2function formatDinosaurData(value: string | number | boolean): string {
3  if (typeof value === "string") {
4    // TypeScript knows that value is of type string here
5    return value.toUpperCase();
6  }
7  if (typeof value === "number") {
8    // TypeScript knows that value is of type number here
9    return value.toFixed(2) + " kg";
10  }
11  // TypeScript knows that value is of type boolean here
12  return value ? "Active" : "Inactive";
13}
14
15console.log(formatDinosaurData("Tyrannosaurus Rex")); // "TYRANNOSAURUS REX"
16console.log(formatDinosaurData(7500));                 // "7500.00 kg"
17console.log(formatDinosaurData(true));                 // "Active"

Dans l'exemple ci-dessus, TypeScript restreint automatiquement le type de value dans chaque bloc if. Après avoir vérifié typeof value === "string", le compilateur sait qu'à l'intérieur de ce bloc value est une chaîne de caractères, donc tu peux appeler .toUpperCase() en toute sécurité. Cela fonctionne avec les types "string", "number", "boolean", "bigint", "symbol", "undefined", "object" et "function".

L'opérateur instanceof

Lorsqu'on travaille avec des classes, instanceof permet de vérifier si un objet est une instance d'une classe précise. TypeScript restreint automatiquement le type après une telle vérification.

1class Carnivore {
2  name: string;
3  biteForce: number;
4
5  constructor(name: string, biteForce: number) {
6    this.name = name;
7    this.biteForce = biteForce;
8  }
9
10  hunt(): string {
11    return `${this.name} hunts with bite force of ${this.biteForce}N!`;
12  }
13}
14
15class Herbivore {
16  name: string;
17  favoriteFood: string;
18
19  constructor(name: string, favoriteFood: string) {
20    this.name = name;
21    this.favoriteFood = favoriteFood;
22  }
23
24  graze(): string {
25    return `${this.name} calmly eats ${this.favoriteFood}`;
26  }
27}
28
29// Function handling both dinosaur types
30function handleDinosaur(dino: Carnivore | Herbivore): string {
31  if (dino instanceof Carnivore) {
32    // TypeScript knows that dino is Carnivore here
33    return dino.hunt(); // We have access to hunt() and biteForce
34  }
35  // TypeScript knows that dino is Herbivore here
36  return dino.graze(); // We have access to graze() and favoriteFood
37}
38
39const rex = new Carnivore("T-Rex", 12800);
40const trike = new Herbivore("Triceratops", "ferns");
41
42console.log(handleDinosaur(rex));   // "T-Rex hunts with bite force of 12800N!"
43console.log(handleDinosaur(trike)); // "Triceratops calmly eats ferns"

L'opérateur in

L'opérateur in vérifie si un objet possède une propriété donnée. TypeScript s'en sert pour restreindre les types au sein des types union.

1interface FlyingDinosaur {
2  name: string;
3  maxAltitude: number;
4  wingspan: number;
5}
6
7interface SwimmingDinosaur {
8  name: string;
9  maxDepth: number;
10  swimSpeed: number;
11}
12
13interface LandDinosaur {
14  name: string;
15  runSpeed: number;
16  weight: number;
17}
18
19type Dinosaur = FlyingDinosaur | SwimmingDinosaur | LandDinosaur;
20
21function describeDinosaur(dino: Dinosaur): string {
22  if ("maxAltitude" in dino) {
23    // TypeScript knows this is FlyingDinosaur
24    return `${dino.name} flies up to altitude of ${dino.maxAltitude}m, wingspan: ${dino.wingspan}m`;
25  }
26  if ("maxDepth" in dino) {
27    // TypeScript knows this is SwimmingDinosaur
28    return `${dino.name} dives to ${dino.maxDepth}m, speed: ${dino.swimSpeed} km/h`;
29  }
30  // TypeScript knows this is LandDinosaur
31  return `${dino.name} runs at speed of ${dino.runSpeed} km/h, weighs ${dino.weight} kg`;
32}
33
34const pteranodon: FlyingDinosaur = { name: "Pteranodon", maxAltitude: 500, wingspan: 7 };
35const mosasaurus: SwimmingDinosaur = { name: "Mosasaurus", maxDepth: 200, swimSpeed: 48 };
36const velociraptor: LandDinosaur = { name: "Velociraptor", runSpeed: 64, weight: 15 };
37
38console.log(describeDinosaur(pteranodon));
39console.log(describeDinosaur(mosasaurus));
40console.log(describeDinosaur(velociraptor));

L'opérateur in est particulièrement utile lorsqu'on travaille avec des interfaces (et non des classes), car instanceof ne fonctionne qu'avec les classes, tandis que in se contente de vérifier la présence d'une propriété.

Fonctions de garde de type personnalisées (le mot-clé is)

Tu peux créer tes propres fonctions de garde de type qui renvoient le type spécial nomDuParametre is Type. Ainsi, TypeScript comprend qu'après une vérification positive, la variable a un type précis.

1interface DinoEgg {
2  species: string;
3  weight: number;
4  incubationDays: number;
5}
6
7interface HatchedDino {
8  species: string;
9  weight: number;
10  age: number;
11  enclosureId: string;
12}
13
14// Custom type guard - function returns "specimen is HatchedDino"
15function isHatched(specimen: DinoEgg | HatchedDino): specimen is HatchedDino {
16  return "age" in specimen && "enclosureId" in specimen;
17}
18
19function processSpecimen(specimen: DinoEgg | HatchedDino): string {
20  if (isHatched(specimen)) {
21    // TypeScript knows that specimen is HatchedDino here
22    return `${specimen.species} - age: ${specimen.age} days, enclosure: ${specimen.enclosureId}`;
23  }
24  // TypeScript knows that specimen is DinoEgg here
25  return `${specimen.species} egg - incubation: ${specimen.incubationDays} days`;
26}
27
28// More advanced type guard with validation
29function isCarnivoreData(data: unknown): data is { species: string; biteForce: number; diet: "carnivore" } {
30  return (
31    typeof data === "object" &&
32    data !== null &&
33    "species" in data &&
34    "biteForce" in data &&
35    "diet" in data &&
36    (data as any).diet === "carnivore"
37  );
38}
39
40// Usage with an unknown data source (e.g., API)
41function processParkData(rawData: unknown): string {
42  if (isCarnivoreData(rawData)) {
43    // TypeScript knows that rawData has fields species, biteForce, diet
44    return `Predator: ${rawData.species}, force: ${rawData.biteForce}N`;
45  }
46  return "Unknown data type";
47}
48
49console.log(processParkData({ species: "T-Rex", biteForce: 12800, diet: "carnivore" }));
50console.log(processParkData({ name: "Triceratops" }));

Les gardes de type personnalisés sont particulièrement utiles lorsque :

  1. La logique de vérification est complexe et que tu veux l'extraire dans une fonction séparée
  2. Tu travailles avec des données provenant de sources externes (API, fichiers JSON), dont le type est unknown
  3. Tu veux créer des outils de validation réutilisables

Unions discriminées et vérifications exhaustives

Les unions discriminées sont un motif où chaque type de l'union possède un champ « discriminant » commun (étiquette) qui identifie le type de manière unique. TypeScript excelle à restreindre les types en fonction de ce champ.

1// Each type has a "kind" field as the discriminator
2interface SecurityAlert {
3  kind: "security";
4  zone: string;
5  threatLevel: number;
6  dinosaurId: string;
7}
8
9interface MedicalAlert {
10  kind: "medical";
11  dinosaurId: string;
12  symptoms: string[];
13  veterinarianRequired: boolean;
14}
15
16interface SystemAlert {
17  kind: "system";
18  component: string;
19  errorCode: number;
20  message: string;
21}
22
23type ParkAlert = SecurityAlert | MedicalAlert | SystemAlert;
24
25function handleAlert(alert: ParkAlert): string {
26  switch (alert.kind) {
27    case "security":
28      // TypeScript knows this is SecurityAlert
29      return `SECURITY ALARM! Zone: ${alert.zone}, Threat: ${alert.threatLevel}/10, Dinosaur: ${alert.dinosaurId}`;
30    case "medical":
31      // TypeScript knows this is MedicalAlert
32      return `MEDICAL ALERT! Dinosaur: ${alert.dinosaurId}, Symptoms: ${alert.symptoms.join(", ")}`;
33    case "system":
34      // TypeScript knows this is SystemAlert
35      return `FAILURE SYSTEM! Component: ${alert.component}, Code: ${alert.errorCode}`;
36  }
37}

Vérification exhaustive avec le type never

Le type never en TypeScript signifie « une valeur qui ne se produira jamais ». On peut l'utiliser pour vérifier qu'on a bien géré TOUTES les variantes d'une union discriminée. Si on ajoute une nouvelle variante et qu'on oublie de la gérer, TypeScript déclenchera une erreur de compilation.

1// Exhaustive check - TypeScript will warn us if we miss any variant
2function handleAlertExhaustive(alert: ParkAlert): string {
3  switch (alert.kind) {
4    case "security":
5      return `Security: ${alert.zone}`;
6    case "medical":
7      return `Medical: ${alert.dinosaurId}`;
8    case "system":
9      return `System: ${alert.component}`;
10    default:
11      // If we add a new alert type and forget to handle it,
12      // TypeScript will report an error, because alert won't be of type never
13      const exhaustiveCheck: never = alert;
14      return exhaustiveCheck;
15  }
16}
17
18// Practical helper for exhaustive check
19function assertNever(value: never, message: string = "Unhandled variant"): never {
20  throw new Error(`${message}: ${JSON.stringify(value)}`);
21}

La vérification exhaustive est une technique puissante - lorsque tu ajoutes par ex. WeatherAlert à l'union ParkAlert, le compilateur t'indiquera IMMÉDIATEMENT tous les endroits du code où la gestion de la nouvelle variante manque. C'est comme un système d'alerte précoce à Jurassic Park - il ne laisse passer aucune menace.

Combiner les gardes de type - exemple pratique

Voici un exemple plus élaboré qui combine différentes techniques de restriction de type dans un même système :

1// Laboratory sample classification system
2type SampleStatus = "collected" | "analyzing" | "completed" | "contaminated";
3
4interface BaseSample {
5  id: string;
6  collectedAt: Date;
7  status: SampleStatus;
8}
9
10interface DNASample extends BaseSample {
11  type: "dna";
12  species: string;
13  purity: number;
14  sequenceLength: number;
15}
16
17interface BloodSample extends BaseSample {
18  type: "blood";
19  species: string;
20  volumeMl: number;
21  pH: number;
22}
23
24interface FossilSample extends BaseSample {
25  type: "fossil";
26  era: string;
27  estimatedAge: number;
28}
29
30type LabSample = DNASample | BloodSample | FossilSample;
31
32// Custom type guard
33function isDNASample(sample: LabSample): sample is DNASample {
34  return sample.type === "dna";
35}
36
37// Checking status using typeof and custom guard
38function canProcess(sample: LabSample): boolean {
39  return sample.status === "collected" || sample.status === "analyzing";
40}
41
42// Main processing function - combines different techniques
43function processLabSample(sample: LabSample): string {
44  // Discriminated union narrowing
45  switch (sample.type) {
46    case "dna":
47      if (sample.purity < 50) {
48        return `DNA Sample ${sample.id}: purity too low (${sample.purity}%)`;
49      }
50      return `DNA Analysis ${sample.species}: ${sample.sequenceLength} base pairs, purity ${sample.purity}%`;
51
52    case "blood":
53      const phStatus = sample.pH >= 7.0 && sample.pH <= 7.6 ? "normal" : "OUT OF RANGE";
54      return `Blood analysis ${sample.species}: ${sample.volumeMl}ml, pH ${sample.pH} (${phStatus})`;
55
56    case "fossil":
57      return `Fossil from era ${sample.era}: estimated age ${sample.estimatedAge} million years`;
58
59    default:
60      const _exhaustive: never = sample;
61      return _exhaustive;
62  }
63}
64
65// Test
66const dnaSample: DNASample = {
67  id: "DNA-042", type: "dna", collectedAt: new Date(),
68  status: "collected", species: "Velociraptor", purity: 87, sequenceLength: 75000000
69};
70
71const bloodSample: BloodSample = {
72  id: "BLD-015", type: "blood", collectedAt: new Date(),
73  status: "analyzing", species: "Triceratops", volumeMl: 120, pH: 7.2
74};
75
76const fossilSample: FossilSample = {
77  id: "FOS-003", type: "fossil", collectedAt: new Date(),
78  status: "completed", era: "Cretaceous", estimatedAge: 68
79};
80
81console.log(processLabSample(dnaSample));
82console.log(processLabSample(bloodSample));
83console.log(processLabSample(fossilSample));

Résumé

La restriction de type et les gardes de type sont des outils fondamentaux de TypeScript qui te permettent d'écrire du code sûr tout en conservant la flexibilité des types union :

  1. typeof - restreint les types primitifs (string, number, boolean)
  2. instanceof - restreint les types de classe
  3. in - restreint les types en fonction de la présence d'une propriété dans un objet
  4. Gardes de type personnalisés (is) - tu définis tes propres fonctions de vérification de type
  5. Unions discriminées - un champ « étiquette » commun identifie sans ambiguïté une variante
  6. Vérification exhaustive avec never - le compilateur garantit que tu as géré toutes les variantes

Comme le dit Robert Muldoon : « L'identification de l'espèce est la première étape du protocole de sécurité. De même, la restriction de type est la première étape vers un code sûr - ne suppose jamais à quoi tu as affaire. Vérifie, restreins le type, et agis en toute confiance. »

Code de cette leçon : index.ts
1// Type narrowing et type guards - Parc Jurassique
2console.log("=== TYPE NARROWING ET TYPE GUARDS ===\n");
3
4// ===========================================
5// 1. typeof - restriction des types primitifs
6// ===========================================
7
8function formatDinoData(value: string | number | boolean): string {
9  if (typeof value === "string") {
10    return value.toUpperCase();
11  }
12  if (typeof value === "number") {
13    return value.toFixed(2) + " kg";
14  }
15  return value ? "Active" : "Inactif";
16}
17
18console.log("--- typeof ---");
19console.log(formatDinoData("Tyrannosaurus Rex"));
20console.log(formatDinoData(7500));
21console.log(formatDinoData(true));
22
23// ===========================================
24// 2. instanceof - restriction des classes
25// ===========================================
26
27class Carnivore {
28  constructor(public name: string, public biteForce: number) {}
29  hunt(): string { return `${this.name} chasse avec une force de ${this.biteForce}N!`; }
30}
31
32class Herbivore {
33  constructor(public name: string, public food: string) {}
34  graze(): string { return `${this.name} eats ${this.food}`; }
35}
36
37function handleDino(dino: Carnivore | Herbivore): string {
38  if (dino instanceof Carnivore) {
39    return dino.hunt();
40  }
41  return dino.graze();
42}
43
44console.log("\n--- instanceof ---");
45console.log(handleDino(new Carnivore("T-Rex", 12800)));
46console.log(handleDino(new Herbivore("Triceratops", "fougères")));
47
48// ===========================================
49// 3. in - vérification des propriétés
50// ===========================================
51
52interface FlyingDino { name: string; maxAltitude: number; }
53interface SwimmingDino { name: string; maxDepth: number; }
54interface LandDino { name: string; runSpeed: number; }
55
56type AnyDino = FlyingDino | SwimmingDino | LandDino;
57
58function describeDino(dino: AnyDino): string {
59  if ("maxAltitude" in dino) return `${dino.name} vole jusqu'à ${dino.maxAltitude}m`;
60  if ("maxDepth" in dino) return `${dino.name} plonge jusqu'à ${dino.maxDepth}m`;
61  return `${dino.name} court à ${dino.runSpeed} km/h`;
62}
63
64console.log("\n--- in operator ---");
65console.log(describeDino({ name: "Pteranodon", maxAltitude: 500 }));
66console.log(describeDino({ name: "Mosasaurus", maxDepth: 200 }));
67console.log(describeDino({ name: "Velociraptor", runSpeed: 64 }));
68
69// ===========================================
70// 4. Custom type guard (is)
71// ===========================================
72
73interface DinoEgg { species: string; incubationDays: number; }
74interface HatchedDino { species: string; age: number; enclosureId: string; }
75
76function isHatched(s: DinoEgg | HatchedDino): s is HatchedDino {
77  return "age" in s && "enclosureId" in s;
78}
79
80console.log("\n--- Custom type guard ---");
81const egg: DinoEgg = { species: "Raptor", incubationDays: 45 };
82const baby: HatchedDino = { species: "Raptor", age: 10, enclosureId: "E-01" };
83console.log("Egg hatched?", isHatched(egg));
84console.log("Baby hatched?", isHatched(baby));
85
86// ===========================================
87// 5. Union discriminée + vérification exhaustive
88// ===========================================
89
90interface SecurityAlert { kind: "security"; zone: string; level: number; }
91interface MedicalAlert { kind: "medical"; dinoId: string; symptoms: string[]; }
92interface SystemAlert { kind: "system"; component: string; errorCode: number; }
93
94type ParkAlert = SecurityAlert | MedicalAlert | SystemAlert;
95
96function handleAlert(alert: ParkAlert): string {
97  switch (alert.kind) {
98    case "security": return `SÉCURITÉ: zone ${alert.zone}, niveau ${alert.level}`;
99    case "medical": return `MÉDICAL: dino ${alert.dinoId}, symptômes: ${alert.symptoms.join(", ")}`;
100    case "system": return `SYSTÈME: ${alert.component}, code ${alert.errorCode}`;
101    default:
102      const _never: never = alert;
103      return _never;
104  }
105}
106
107console.log("\n--- Discriminated union ---");
108console.log(handleAlert({ kind: "security", zone: "A", level: 9 }));
109console.log(handleAlert({ kind: "medical", dinoId: "D-01", symptoms: ["fièvre"] }));
110console.log(handleAlert({ kind: "system", component: "Fence", errorCode: 500 }));
111
112console.log("\nType narrowing = code sécurisé avec union types!");

Tu as repéré une erreur dans cette leçon ?

Teste tes connaissances

Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.

  1. 1. Quel opérateur permet de restreindre un type primitif (par ex. string | number) en TypeScript ?

Tâches pratiques dans le jeu

  • Éditeur de code

    Utilise typeof, instanceof et l'opérateur in pour restreindre les types dans le système de Jurassic Park

  • Clique dans l'ordre

    Range les éléments d'une fonction de garde de type personnalisée :

Articles utiles