Cours JavaScript et TypeScript · Module 7: TypeScript avancé

Types conditionnels

16 min de lecture
Dans cette leçon8

« Dr Wu, nous avons une situation », dit un assistant de laboratoire nerveux. « Le patron d'ADN que nous essayons de séquencer pourrait provenir d'une espèce de dinosaure prédatrice ou herbivore, mais nous avons besoin de protocoles de traitement différents selon le cas. »

Le Dr Henry Wu, généticien en chef de Jurassic Park, répond calmement : « Ce n'est pas un problème. Nos algorithmes de séquençage sont assez intelligents pour détecter les marqueurs de prédation dans l'ADN et appliquer automatiquement le protocole approprié. C'est ça, la flexibilité de notre système. »

Tout comme les algorithmes avancés de séquençage de l'ADN à Jurassic Park, TypeScript dispose d'un mécanisme appelé types conditionnels qui permet de déterminer dynamiquement des types en fonction d'autres types. Cet outil permet de créer des API plus flexibles et plus précises qui s'adaptent à différents scénarios.

Les bases des types conditionnels

Les types conditionnels en TypeScript fonctionnent de façon similaire aux expressions conditionnelles en JavaScript, mais au niveau des types. Leur syntaxe rappelle l'opérateur ternaire :

1T extends U ? X : Y

Cela se lit ainsi : « Si le type T étend (est compatible avec) le type U, alors le résultat est le type X, sinon le type Y ».

Exemple simple

Créons une fonction simple qui traite un objet représentant un dinosaure et renvoie des résultats différents selon son régime alimentaire :

1type Carnivore = { type: "carnivore"; huntingSkill: number };
2type Herbivore = { type: "herbivore"; foragingSkill: number };
3
4type Dinosaur = Carnivore | Herbivore;
5
6// Conditional type determining the appropriate result type
7type DinosaurActivity<T extends Dinosaur> = T extends Carnivore
8  ? { activity: "hunting"; preyTargeted: string }
9  : { activity: "grazing"; plantsConsumed: number };
10
11// Function using the conditional type
12function trackDinosaurActivity<T extends Dinosaur>(dino: T): DinosaurActivity<T> {
13  if (dino.type === "carnivore") {
14    // TypeScript knows that for Carnivore we should return { activity: "hunting"; preyTargeted: string }
15    return {
16      activity: "hunting",
17      preyTargeted: "Gallimimus"
18    } as DinosaurActivity<T>;
19  } else {
20    // TypeScript knows that for Herbivore we should return { activity: "grazing"; plantsConsumed: number }
21    return {
22      activity: "grazing",
23      plantsConsumed: 42
24    } as DinosaurActivity<T>;
25  }
26}
27
28// Example usage
29const trex: Carnivore = { type: "carnivore", huntingSkill: 10 };
30const triceratops: Herbivore = { type: "herbivore", foragingSkill: 8 };
31
32const trexActivity = trackDinosaurActivity(trex);
33console.log(trexActivity.activity); // "hunting"
34console.log(trexActivity.preyTargeted); // "Gallimimus"
35
36const triceratopsActivity = trackDinosaurActivity(triceratops);
37console.log(triceratopsActivity.activity); // "grazing"
38console.log(triceratopsActivity.plantsConsumed); // 42

Dans cet exemple, le type DinosaurActivity<T> change selon que T étend Carnivore ou non. Cela nous permet de renvoyer des structures de données différentes, et TypeScript fournit un support de type complet pour les deux cas.

Types conditionnels distributifs

L'une des caractéristiques clés des types conditionnels est leur comportement distributif avec les types union. Lorsqu'on applique un type conditionnel à un type union, la condition est appliquée séparément à chaque élément de l'union.

1type ToArray<T> = T extends any ? T[] : never;
2
3// With a simple type, the result is simple
4type NumberArray = ToArray<number>; // number[]
5
6// With a union type, the condition is applied distributively
7type NumberOrStringArray = ToArray<number | string>; // number[] | string[]

Dans le second cas, ToArray<number | string> équivaut à ToArray<number> | ToArray<string>, ce qui donne number[] | string[], et non (number | string)[].

On peut appliquer cela à notre exemple de dinosaure :

1// Defining more dinosaur types
2type TRex = { species: "tyrannosaurus"; strength: number };
3type Velociraptor = { species: "velociraptor"; speed: number };
4type Triceratops = { species: "triceratops"; armorLevel: number };
5type Brachiosaurus = { species: "brachiosaurus"; height: number };
6
7// Conditional type filtering predators
8type IsPredator<T> = T extends { species: "tyrannosaurus" | "velociraptor" } ? T : never;
9
10// Distributive application on union types
11type Predators = IsPredator<TRex | Velociraptor | Triceratops | Brachiosaurus>;
12// Result: TRex | Velociraptor
13
14// We can also filter herbivores
15type IsHerbivore<T> = T extends { species: "triceratops" | "brachiosaurus" } ? T : never;
16type Herbivores = IsHerbivore<TRex | Velociraptor | Triceratops | Brachiosaurus>;
17// Result: Triceratops | Brachiosaurus

Types inférés

TypeScript permet d'extraire (d'inférer) des types depuis d'autres types à l'aide du mot-clé infer. C'est particulièrement utile dans les types conditionnels.

1// Type that extracts the return type of a function
2type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
3
4// Example usage
5function studyDinosaur(specimen: string): { species: string; age: number } {
6  return {
7    species: "Velociraptor",
8    age: 5
9  };
10}
11
12// Extracted return type of the studyDinosaur function
13type StudyResult = ReturnType<typeof studyDinosaur>; // { species: string; age: number }

Dans cet exemple, infer R indique à TypeScript de « déduire » le type de retour de la fonction et de l'assigner au type R, qui est ensuite utilisé comme résultat du type conditionnel.

On peut créer des exemples d'extraction de type plus complexes, par exemple :

1// Type representing dinosaur DNA
2type DinosaurDNA = {
3  species: string;
4  genomeSequence: string;
5  markers: {
6    predatory?: boolean;
7    aquatic?: boolean;
8    flying?: boolean;
9    size: "small" | "medium" | "large";
10  };
11};
12
13// Extracting markers from DNA
14type ExtractMarkers<T> = T extends { markers: infer M } ? M : never;
15
16// Extracting size from markers
17type ExtractSize<T> = T extends { size: infer S } ? S : never;
18
19// Example usage
20type DinosaurMarkers = ExtractMarkers<DinosaurDNA>;
21// { predatory?: boolean; aquatic?: boolean; flying?: boolean; size: "small" | "medium" | "large"; }
22
23type DinosaurSize = ExtractSize<DinosaurMarkers>;
24// "small" | "medium" | "large"

Applications pratiques des types conditionnels

Les types conditionnels sont particulièrement utiles dans des scénarios de programmation avancés, tels que :

1. Créer des composants d'API flexibles

1// Example API for dinosaur operations
2interface DinosaurOperations<T> {
3  create: (data: T) => Promise<T>;
4  update: (id: string, data: Partial<T>) => Promise<T>;
5  delete: (id: string) => Promise<void>;
6
7  // Method that returns different types depending on parameter T
8  analyze: T extends { genomeSequence: string }
9    ? (id: string) => Promise<{ compatibilityScore: number }>
10    : (id: string) => Promise<{ behaviorPrediction: string }>;
11}
12
13// Implementation for dinosaurs with genome sequence
14interface GenomeSequencedDino {
15  id: string;
16  name: string;
17  genomeSequence: string;
18}
19
20// Implementation for dinosaurs without genome sequence
21interface BehaviorStudiedDino {
22  id: string;
23  name: string;
24  observedBehaviors: string[];
25}
26
27// Different implementations for different types
28function createDinosaurAPI<T extends GenomeSequencedDino | BehaviorStudiedDino>(
29  baseUrl: string
30): DinosaurOperations<T> {
31  return {
32    create: (data: T) => fetch(`${baseUrl}/create`, {
33      method: 'POST',
34      body: JSON.stringify(data)
35    }).then(res => res.json()),
36
37    update: (id: string, data: Partial<T>) => fetch(`${baseUrl}/${id}`, {
38      method: 'PUT',
39      body: JSON.stringify(data)
40    }).then(res => res.json()),
41
42    delete: (id: string) => fetch(`${baseUrl}/${id}`, {
43      method: 'DELETE'
44    }).then(() => undefined),
45
46    // The analyze method implementation will differ depending on type T
47    analyze: ((id: string) => {
48      if ('genomeSequence' in { ...({} as T) }) {
49        return fetch(`${baseUrl}/${id}/genome-analysis`)
50          .then(res => res.json());
51      } else {
52        return fetch(`${baseUrl}/${id}/behavior-analysis`)
53          .then(res => res.json());
54      }
55    }) as any // Cast needed due to TypeScript runtime limitations
56  };
57}
58
59// Using different APIs
60const genomeAPI = createDinosaurAPI<GenomeSequencedDino>('/api/genome-dinos');
61const result1 = await genomeAPI.analyze('dino-1'); // { compatibilityScore: number }
62
63const behaviorAPI = createDinosaurAPI<BehaviorStudiedDino>('/api/behavior-dinos');
64const result2 = await behaviorAPI.analyze('dino-2'); // { behaviorPrediction: string }

2. Implémenter des motifs de type

1// Helper type Exclude - excludes types from a union
2type Exclude<T, U> = T extends U ? never : T;
3
4// Example: excluding dangerous dinosaurs
5type DangerousDinosaurs = "Tyrannosaurus" | "Velociraptor" | "Spinosaurus";
6type AllDinosaurs = "Tyrannosaurus" | "Triceratops" | "Velociraptor" | "Brachiosaurus" | "Stegosaurus" | "Spinosaurus";
7
8type SafeDinosaurs = Exclude<AllDinosaurs, DangerousDinosaurs>;
9// "Triceratops" | "Brachiosaurus" | "Stegosaurus"
10
11// Helper type Extract - extracts types meeting a condition
12type Extract<T, U> = T extends U ? T : never;
13
14// Example: extracting dinosaurs from the Jurassic era
15type JurassicDinosaurs = "Stegosaurus" | "Brachiosaurus" | "Allosaurus";
16type CretaceousDinosaurs = "Tyrannosaurus" | "Triceratops" | "Velociraptor";
17type AllEras = JurassicDinosaurs | CretaceousDinosaurs;
18
19type OnlyJurassic = Extract<AllEras, JurassicDinosaurs>;
20// "Stegosaurus" | "Brachiosaurus" | "Allosaurus"
21
22// Helper type NonNullable - removes null and undefined from a type
23type NonNullable<T> = T extends null | undefined ? never : T;
24
25// Example: ensuring we have non-empty dinosaur data
26type PossibleDinoData = { name: string; weight: number } | null | undefined;
27type DefiniteDinoData = NonNullable<PossibleDinoData>;
28// { name: string; weight: number }

3. Créer des mappeurs de type

1// Type Mapper transforming object properties from one type to another
2type MapProperties<T, U> = {
3  [K in keyof T]: T[K] extends object ? MapProperties<T[K], U> : U;
4};
5
6// Example: mapping numeric values to strings
7type DinosaurStats = {
8  name: string;
9  physicalTraits: {
10    height: number;
11    weight: number;
12    speed: number;
13  };
14  behavioralTraits: {
15    aggression: number;
16    intelligence: number;
17    packBehavior: number;
18  };
19};
20
21// Transforming all numbers to strings for display purposes
22type DinosaurStatsForDisplay = MapProperties<DinosaurStats, string>;
23/*
24{
25  name: string;
26  physicalTraits: {
27    height: string;
28    weight: string;
29    speed: string;
30  };
31  behavioralTraits: {
32    aggression: string;
33    intelligence: string;
34    packBehavior: string;
35  };
36}
37*/

Exemple avancé : système de types pour l'infrastructure de Jurassic Park

Voyons un exemple plus complexe qui utilise des types conditionnels pour modéliser différents systèmes de Jurassic Park :

1// --- Basic system types ---
2
3type SystemStatus = "online" | "offline" | "maintenance" | "error";
4
5interface BaseSystem {
6  id: string;
7  name: string;
8  status: SystemStatus;
9  lastUpdated: Date;
10}
11
12// Different types of systems in the park
13interface SecuritySystem extends BaseSystem {
14  type: "security";
15  enclosures: string[];
16  alertLevel: "low" | "medium" | "high" | "critical";
17  cameras: number;
18  motionSensors: number;
19}
20
21interface PowerSystem extends BaseSystem {
22  type: "power";
23  outputCapacity: number; // kW
24  currentLoad: number; // kW
25  backupCapacity: number; // kW
26  generators: number;
27}
28
29interface TransportSystem extends BaseSystem {
30  type: "transport";
31  vehicles: number;
32  routes: string[];
33  passengerCapacity: number;
34  automatedGuidance: boolean;
35}
36
37interface LifeSupportSystem extends BaseSystem {
38  type: "lifesupport";
39  habitats: string[];
40  environmentalControls: {
41    temperature: number; // °C
42    humidity: number; // %
43    oxygen: number; // %
44  };
45}
46
47// Union of all system types
48type ParkSystem = SecuritySystem | PowerSystem | TransportSystem | LifeSupportSystem;
49
50// --- Conditional types for monitoring data ---
51
52/**
53 * Conditional type that determines what monitoring data is relevant for each system type
54 */
55type MonitoringData<T extends ParkSystem> = T extends SecuritySystem
56  ? {
57      breachDetections: number;
58      securityFootage: { cameraId: string; timestamp: Date; events: string[] }[];
59      activeAlarms: string[];
60    }
61  : T extends PowerSystem
62  ? {
63      loadDistribution: { sector: string; load: number }[];
64      energyEfficiency: number; // %
65      estimatedBackupTime: number; // hours
66      powerFluctuations: boolean;
67    }
68  : T extends TransportSystem
69  ? {
70      activeVehicles: number;
71      passengerCount: number;
72      delayReports: { routeId: string; delay: number }[];
73      fuelConsumption: number; // liters/hour
74    }
75  : T extends LifeSupportSystem
76  ? {
77      habitatConditions: {
78        habitatId: string;
79        temperature: number;
80        humidity: number;
81        oxygenLevel: number;
82        co2Level: number;
83      }[];
84      anomalyDetections: string[];
85      resourceConsumption: { water: number; electricity: number; air: number };
86    }
87  : never; // Fallback pour les types inconnus
88
89/**
90 * Conditional type determining recommended actions for systems in different states
91 */
92type RecommendedActions<T extends ParkSystem> =
93  T extends { status: "error" }
94    ? { severity: "high"; actions: string[]; escalation: string; timeToResolve: number }
95    : T extends { status: "maintenance" }
96    ? { checklistItems: string[]; estimatedCompletionTime: Date; personnelAssigned: string[] }
97    : T extends { status: "offline" }
98    ? { restartProcedures: string[]; systemDependencies: string[]; estimatedDowntime: number }
99    : { routineChecks: string[]; nextMaintenanceDate: Date };
100
101// --- System monitoring class using conditional types ---
102
103/**
104 * Class managing system monitoring, using conditional types
105 * to ensure type correctness for different systems
106 */
107class SystemMonitor<T extends ParkSystem> {
108  private system: T;
109  private monitoringData: MonitoringData<T>;
110  private recommendedActions: RecommendedActions<T>;
111
112  constructor(system: T) {
113    this.system = system;
114
115    // Initializing monitoringData based on system type
116    this.monitoringData = this.initializeMonitoringData();
117
118    // Initializing recommendedActions based on system type and status
119    this.recommendedActions = this.initializeRecommendedActions();
120  }
121
122  // Helper method for initializing monitoring data
123  private initializeMonitoringData(): MonitoringData<T> {
124    // Implementation dependent on type T
125    if (this.system.type === "security") {
126      return {
127        breachDetections: 0,
128        securityFootage: [],
129        activeAlarms: []
130      } as MonitoringData<T>;
131    } else if (this.system.type === "power") {
132      return {
133        loadDistribution: [],
134        energyEfficiency: 95,
135        estimatedBackupTime: 48,
136        powerFluctuations: false
137      } as MonitoringData<T>;
138    } else if (this.system.type === "transport") {
139      return {
140        activeVehicles: 0,
141        passengerCount: 0,
142        delayReports: [],
143        fuelConsumption: 0
144      } as MonitoringData<T>;
145    } else if (this.system.type === "lifesupport") {
146      return {
147        habitatConditions: [],
148        anomalyDetections: [],
149        resourceConsumption: { water: 0, electricity: 0, air: 0 }
150      } as MonitoringData<T>;
151    } else {
152      throw new Error(`Unknown system type: ${(this.system as any).type}`);
153    }
154  }
155
156  // Helper method for initializing recommended actions
157  private initializeRecommendedActions(): RecommendedActions<T> {
158    if (this.system.status === "error") {
159      return {
160        severity: "high",
161        actions: ["Immediate technical intervention", "Notify management"],
162        escalation: "Technical Director",
163        timeToResolve: 60 // minutes
164      } as RecommendedActions<T>;
165    } else if (this.system.status === "maintenance") {
166      return {
167        checklistItems: ["Check wiring", "Test functionality"],
168        estimatedCompletionTime: new Date(Date.now() + 3600000), // +1 hour
169        personnelAssigned: ["Tech-1", "Tech-2"]
170      } as RecommendedActions<T>;
171    } else if (this.system.status === "offline") {
172      return {
173        restartProcedures: ["Power control", "System reset"],
174        systemDependencies: ["Main power supply"],
175        estimatedDowntime: 120 // minutes
176      } as RecommendedActions<T>;
177    } else {
178      return {
179        routineChecks: ["Daily overview", "Check logs"],
180        nextMaintenanceDate: new Date(Date.now() + 7 * 24 * 3600000) // +7 days
181      } as RecommendedActions<T>;
182    }
183  }
184
185  // Method for updating monitoring data
186  updateMonitoringData(newData: Partial<MonitoringData<T>>): void {
187    this.monitoringData = { ...this.monitoringData, ...newData };
188    console.log(`Updated monitoring data for ${this.system.name} (ID: ${this.system.id})`);
189  }
190
191  // Method for generating system report
192  generateSystemReport(): string {
193    const report = `
194SYSTEM REPORT: ${this.system.name.toUpperCase()} (ID: ${this.system.id})
195=========================================================
196Type: ${this.system.type}
197Status: ${this.system.status}
198Last update: ${this.system.lastUpdated.toLocaleString()}
199
200MONITORING DETAILS:
201${this.formatMonitoringData()}
202
203RECOMMENDED ACTIONS:
204${this.formatRecommendedActions()}
205=========================================================
206Report generated: ${new Date().toLocaleString()}
207`;
208
209    return report;
210  }
211
212  // Method for formatting monitoring data depending on system type
213  private formatMonitoringData(): string {
214    if (this.system.type === "security") {
215      const data = this.monitoringData as MonitoringData<SecuritySystem>;
216      return `
217Detected violations: ${data.breachDetections}
218Active alarms: ${data.activeAlarms.length > 0 ? data.activeAlarms.join(", ") : "None"}
219Security recordings: ${data.securityFootage.length}
220`;
221    } else if (this.system.type === "power") {
222      const data = this.monitoringData as MonitoringData<PowerSystem>;
223      return `
224Energy efficiency: ${data.energyEfficiency}%
225Estimated time on backup power: ${data.estimatedBackupTime} hours
226Power fluctuations: ${data.powerFluctuations ? "YES" : "NO"}
227Schedule loads: ${data.loadDistribution.map(d => `${d.sector}: ${d.load}kW`).join(", ")}
228`;
229    } else if (this.system.type === "transport") {
230      const data = this.monitoringData as MonitoringData<TransportSystem>;
231      return `
232Active vehicles: ${data.activeVehicles}
233Number of passengers: ${data.passengerCount}
234Fuel usage: ${data.fuelConsumption} liters/hr.
235Delay reports: ${data.delayReports.length > 0
236  ? data.delayReports.map(d => `${d.routeId}: ${d.delay}min`).join(", ")
237  : "No delays"}
238`;
239    } else if (this.system.type === "lifesupport") {
240      const data = this.monitoringData as MonitoringData<LifeSupportSystem>;
241      return `
242Habitat conditions: ${data.habitatConditions.map(h =>
243  `${h.habitatId} (Temp: ${h.temperature}°C, Humidity: ${h.humidity}%, O2: ${h.oxygenLevel}%)`).join("\n")}
244Detected anomalies: ${data.anomalyDetections.length > 0 ? data.anomalyDetections.join(", ") : "None"}
245Resource usage: Water: ${data.resourceConsumption.water}m³, Power: ${data.resourceConsumption.electricity}kWh, Air: ${data.resourceConsumption.air}m³
246`;
247    } else {
248      return `No monitoring data for this type of system.`;
249    }
250  }
251
252  // Method for formatting recommended actions depending on system status
253  private formatRecommendedActions(): string {
254    if (this.system.status === "error") {
255      const actions = this.recommendedActions as RecommendedActions<{ status: "error" }>;
256      return `
257Severity: ${actions.severity}
258Recommended actions: ${actions.actions.join(", ")}
259Escalation to: ${actions.escalation}
260Estimated repair time: ${actions.timeToResolve} minutes
261`;
262    } else if (this.system.status === "maintenance") {
263      const actions = this.recommendedActions as RecommendedActions<{ status: "maintenance" }>;
264      return `
265Items to check: ${actions.checklistItems.join(", ")}
266Estimated completion time: ${actions.estimatedCompletionTime.toLocaleString()}
267Assigned staff: ${actions.personnelAssigned.join(", ")}
268`;
269    } else if (this.system.status === "offline") {
270      const actions = this.recommendedActions as RecommendedActions<{ status: "offline" }>;
271      return `
272Restart procedures: ${actions.restartProcedures.join(", ")}
273System dependencies: ${actions.systemDependencies.join(", ")}
274Estimated downtime: ${actions.estimatedDowntime} minutes
275`;
276    } else {
277      const actions = this.recommendedActions as RecommendedActions<ParkSystem>;
278      return `
279Routine checks: ${actions.routineChecks.join(", ")}
280Next overview: ${actions.nextMaintenanceDate.toLocaleDateString()}
281`;
282    }
283  }
284
285  // Method returning recommended actions
286  getRecommendedActions(): RecommendedActions<T> {
287    return this.recommendedActions;
288  }
289
290  // Method for updating system status
291  updateSystemStatus(newStatus: SystemStatus): void {
292    (this.system as any).status = newStatus;
293    this.system.lastUpdated = new Date();
294
295    // Updating recommended actions after status change
296    this.recommendedActions = this.initializeRecommendedActions();
297
298    console.log(`Updated system status ${this.system.name} to ${newStatus}`);
299  }
300}
301
302// --- Example usage ---
303
304// Creating sample systems
305const securitySystem: SecuritySystem = {
306  id: "SEC-001",
307  name: "Main security system",
308  status: "online",
309  lastUpdated: new Date(),
310  type: "security",
311  enclosures: ["T-Rex", "Velociraptor", "Dilophosaurus"],
312  alertLevel: "low",
313  cameras: 24,
314  motionSensors: 36
315};
316
317const powerSystem: PowerSystem = {
318  id: "PWR-001",
319  name: "Main power system",
320  status: "online",
321  lastUpdated: new Date(),
322  type: "power",
323  outputCapacity: 10000,
324  currentLoad: 6500,
325  backupCapacity: 5000,
326  generators: 4
327};
328
329// Creating monitors for different systems
330const securityMonitor = new SystemMonitor<SecuritySystem>(securitySystem);
331const powerMonitor = new SystemMonitor<PowerSystem>(powerSystem);
332
333// Updating monitoring data
334securityMonitor.updateMonitoringData({
335  breachDetections: 3,
336  activeAlarms: ["Sector 7G", "Northern fence"],
337  securityFootage: [
338    { cameraId: "CAM-12", timestamp: new Date(), events: ["Motion in restricted area"] }
339  ]
340});
341
342powerMonitor.updateMonitoringData({
343  loadDistribution: [
344    { sector: "Visitor Center", load: 2000 },
345    { sector: "TRex Paddock", load: 1500 },
346    { sector: "Raptor Enclosure", load: 1800 },
347    { sector: "Labs", load: 1200 }
348  ],
349  energyEfficiency: 92,
350  powerFluctuations: true
351});
352
353// Simulating a power system failure
354powerMonitor.updateSystemStatus("error");
355
356// Generating reports
357console.log(securityMonitor.generateSystemReport());
358console.log(powerMonitor.generateSystemReport());
359
360// Checking if recommended actions are of the correct type
361const powerActions = powerMonitor.getRecommendedActions();
362console.log(`Power failure severity: ${powerActions.severity}`);
363console.log(`Time to solutions: ${powerActions.timeToResolve} minutes`);

Cet exemple élaboré montre comment les types conditionnels peuvent être utilisés pour créer un système complexe qui adapte son comportement et sa structure de données selon le type d'objet avec lequel il travaille. C'est particulièrement utile pour modéliser des systèmes ayant de nombreuses variantes mais partageant certaines fonctionnalités communes.

Limites des types conditionnels

Bien que les types conditionnels soient un outil puissant, ils ont aussi quelques limites :

  1. Capacité d'inférence limitée - TypeScript a une capacité limitée à inférer des types dans des scénarios complexes, surtout lorsque des types génériques sont utilisés conjointement avec des types conditionnels.

  2. Effacement de type à l'exécution - comme les autres fonctionnalités de type avancées de TypeScript, les types conditionnels n'existent qu'au moment de la compilation. À l'exécution, il n'y a aucune information sur les types conditionnels, ce qui peut compliquer l'implémentation de certains motifs.

  3. Complexité pour les débutants - les types conditionnels, surtout combinés à d'autres fonctionnalités de type avancées, peuvent être difficiles à comprendre pour les débutants en TypeScript.

Quand utiliser les types conditionnels ?

Les types conditionnels sont particulièrement utiles dans les scénarios suivants :

  1. Créer des API flexibles - lorsque tu veux que tes fonctions ou méthodes se comportent différemment selon le type des arguments passés.

  2. Implémenter des outils génériques avancés - lors de la construction d'outils complexes devant fonctionner avec différents types de données.

  3. Modéliser des systèmes métier complexes - lorsqu'on a affaire à différentes variantes d'objets métier qui partagent des caractéristiques communes mais diffèrent dans les détails.

  4. Créer des outils de transformation de type - lorsqu'il faut mapper un type vers un autre de façon conditionnelle.

Résumé

Les types conditionnels en TypeScript sont un outil avancé mais extrêmement utile qui permet de créer des API flexibles et sûres en matière de types. Tout comme les généticiens de Jurassic Park peuvent adapter leurs protocoles aux différents types d'ADN de dinosaures, les types conditionnels nous permettent d'adapter le comportement de notre code aux différents types de données tout en conservant un support complet du typage statique.

Dans les exercices suivants, nous continuerons à explorer les fonctionnalités avancées du système de types de TypeScript et nous verrons comment elles peuvent nous aider à créer un code plus sûr, plus flexible et plus facile à maintenir.

Code de cette leçon : index.ts
1// Conditional types (Conditional Types) - Parc Jurassique
2console.log("=== CONDITIONAL TYPES ===\n");
3
4// ===========================================
5// 1. Type conditionnel de base
6// ===========================================
7
8type IsCarnivore<T> = T extends { diet: "carnivore" } ? true : false;
9
10interface Carnivore {
11  name: string;
12  diet: "carnivore";
13  biteForce: number;
14}
15
16interface Herbivore {
17  name: string;
18  diet: "herbivore";
19  favoriteFood: string;
20}
21
22// Le type conditionnel se résout à la compilation
23type RexCheck = IsCarnivore<Carnivore>;   // true
24type CeraCheck = IsCarnivore<Herbivore>;  // false
25
26// ===========================================
27// 2. Extract et Exclude
28// ===========================================
29
30type DinosaurType = "T-Rex" | "Velociraptor" | "Triceratops" | "Brachiosaurus" | "Stegosaurus";
31
32// Extract - extraire les types correspondants
33type Predator = Extract<DinosaurType, "T-Rex" | "Velociraptor">;
34// Result: "T-Rex" | "Velociraptor"
35
36// Exclude - retirer les types correspondants
37type Gentle = Exclude<DinosaurType, "T-Rex" | "Velociraptor">;
38// Result: "Triceratops" | "Brachiosaurus" | "Stegosaurus"
39
40// ===========================================
41// 3. infer - extraire des types depuis des constructions
42// ===========================================
43
44type DinosaurData = {
45  name: string;
46  stats: { weight: number; height: number; speed: number };
47  abilities: string[];
48};
49
50// Extraire un type depuis une structure imbriquée
51type StatsType = DinosaurData["stats"]; // { weight: number; height: number; speed: number }
52
53// infer dans les types conditionnels
54type UnpackArray<T> = T extends Array<infer U> ? U : T;
55type AbilityType = UnpackArray<DinosaurData["abilities"]>; // string
56
57type UnpackPromise<T> = T extends Promise<infer U> ? U : T;
58type ResolvedDino = UnpackPromise<Promise<DinosaurData>>; // DinosaurData
59
60// ===========================================
61// 4. ReturnType et Parameters
62// ===========================================
63
64function createDinosaur(name: string, weight: number, isDangerous: boolean) {
65  return {
66    name,
67    weight,
68    isDangerous,
69    createdAt: new Date()
70  };
71}
72
73type DinoResult = ReturnType<typeof createDinosaur>;
74type DinoParams = Parameters<typeof createDinosaur>;
75
76// ===========================================
77// 5. NonNullable
78// ===========================================
79
80type MaybeDino = string | number | null | undefined;
81type DefinitelyDino = NonNullable<MaybeDino>; // string | number
82
83// ===========================================
84// 6. Exemple pratique - système de réponses d'API
85// ===========================================
86
87type ApiResponse<T> = T extends "dinosaur"
88  ? { id: number; name: string; species: string }
89  : T extends "enclosure"
90  ? { id: number; name: string; capacity: number }
91  : T extends "staff"
92  ? { id: number; name: string; role: string }
93  : never;
94
95function getResponse<T extends "dinosaur" | "enclosure" | "staff">(
96  type: T
97): ApiResponse<T> {
98  if (type === "dinosaur") {
99    return { id: 1, name: "Rexy", species: "T-Rex" } as ApiResponse<T>;
100  } else if (type === "enclosure") {
101    return { id: 101, name: "Zone T-Rex", capacity: 2 } as ApiResponse<T>;
102  }
103  return { id: 1001, name: "Alan Grant", role: "Paléontologue" } as ApiResponse<T>;
104}
105
106// ===========================================
107// Test
108// ===========================================
109
110console.log("--- Extract / Exclude ---");
111const predator: Predator = "T-Rex";
112const gentle: Gentle = "Brachiosaurus";
113console.log("Prédateur:", predator);
114console.log("Doux:", gentle);
115
116console.log("\n--- ReturnType ---");
117const dino: DinoResult = createDinosaur("Blue", 15, true);
118console.log("Dinosaure créé:", JSON.stringify(dino, null, 2));
119
120console.log("\n--- Conditional API Response ---");
121const dinoResp = getResponse("dinosaur");
122console.log("Dinosaur:", JSON.stringify(dinoResp));
123
124const encResp = getResponse("enclosure");
125console.log("Enclos:", JSON.stringify(encResp));
126
127const staffResp = getResponse("staff");
128console.log("Employé:", JSON.stringify(staffResp));
129
130console.log("\n--- NonNullable ---");
131function processDino(value: NonNullable<MaybeDino>): void {
132  console.log("Valeur:", value, "Type:", typeof value);
133}
134processDino("Rexy");
135processDino(42);
136// processDino(null); // Error!
137
138console.log("\nConditional types = logique dynamique au niveau des types!");

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

Teste tes connaissances

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

  1. 1. Que fait le type conditionnel T extends U ? X : Y en TypeScript ?

  2. 2. À quoi sert le mot-clé infer dans les types conditionnels ?

Voici 2 des 3 questions de cette leçon. Réponds aux autres dans le jeu.

Tâches pratiques dans le jeu

  • Éditeur de code

    Crée des types conditionnels pour l'inférence dynamique de types

  • Clique dans l'ordre

    Range les éléments d'un type conditionnel :

Articles utiles