Cours JavaScript et TypeScript · Module 10: TypeScript en pratique
Projet final : le système de diagnostic de Jurassic Park
Dans cette leçon4
« Nous voici au terme de notre voyage », annonce le Dr Ellie Sattler, debout dans le laboratoire de diagnostic principal d'InGen. « Tout au long de ce module, tu as maîtrisé les Web Workers, la gestion de la mémoire, les types avancés de TypeScript - des utility types aux conditional et mapped types, jusqu'aux template literal types et au module augmentation. Tu as aussi appris le Test-Driven Development et l'intégration de TypeScript avec les frameworks populaires. »
« Tu vas maintenant construire un système complet de diagnostic de Jurassic Park - un projet qui réunit toutes ces compétences en un système cohérent. Vois-le comme une simulation de code réel, de production, qui pourrait tourner dans notre parc. »
Dans ce projet, tu vas créer un système de diagnostic avancé combinant : un typage de niveau expert avec utility types et conditional types, des template literal types pour la génération d'API, des mapped types pour la transformation des données, du module augmentation pour étendre des bibliothèques existantes, et une approche TDD avec des tests unitaires.
Objectif du projet
Tu vas construire un système DiagnosticCenter qui :
- Utilise des utility types avancés pour modéliser les données avec précision
- Applique des conditional types pour sélectionner automatiquement la stratégie de diagnostic
- Emploie des template literal types pour construire une API type-safe
- Implémente des mapped types pour transformer les données de diagnostic
- Étend des modules existants via le module augmentation
- Est écrit selon une approche TDD avec des tests unitaires
Exigences fonctionnelles
1. Utility types avancés pour les données de diagnostic
Définis des types de données précis, en utilisant à la fois des utility types intégrés et personnalisés. Le système doit prendre en charge différentes catégories de dinosaures avec différents jeux de paramètres de diagnostic.
1// Base dinosaur type
2interface DinosaurRecord {
3 id: string;
4 species: string;
5 name: string;
6 age: number;
7 weight: number;
8 height: number;
9 enclosureId: string;
10 diet: 'carnivore' | 'herbivore' | 'omnivore';
11 healthStatus: 'healthy' | 'sick' | 'critical' | 'quarantined';
12 lastCheckup: Date;
13 geneticModifications: string[];
14 dangerRating: 1 | 2 | 3 | 4 | 5;
15}
16
17// Custom utility types
18type DeepReadonly<T> = {
19 readonly [K in keyof T]: T[K] extends object
20 ? T[K] extends Date | Array<any>
21 ? T[K]
22 : DeepReadonly<T[K]>
23 : T[K];
24};
25
26type RequireAtLeastOne<T, Keys extends keyof T = keyof T> =
27 Pick<T, Exclude<keyof T, Keys>> &
28 { [K in Keys]-?: Required<Pick<T, K>> & Partial<Pick<T, Exclude<Keys, K>>> }[Keys];
29
30type Mutable<T> = {
31 -readonly [K in keyof T]: T[K];
32};
33
34// Locked record - cannot be modified after closing diagnostics
35type LockedDiagnosticRecord = DeepReadonly<DinosaurRecord>;
36
37// Update - require at least one field
38type DinosaurUpdate = RequireAtLeastOne<
39 Partial<Pick<DinosaurRecord, 'healthStatus' | 'weight' | 'enclosureId' | 'dangerRating'>>,
40 'healthStatus' | 'weight' | 'enclosureId' | 'dangerRating'
41>;
42
43// Display record - convert Date to string
44type DisplayRecord<T> = {
45 [K in keyof T]: T[K] extends Date ? string : T[K];
46};
47
48type DinosaurDisplayRecord = DisplayRecord<DinosaurRecord>;
49
50// Filtering numeric fields
51type NumericFields<T> = {
52 [K in keyof T as T[K] extends number ? K : never]: T[K];
53};
54
55type DinosaurMetrics = NumericFields<DinosaurRecord>;
56// Result: { age: number; weight: number; height: number; dangerRating: 1|2|3|4|5 }2. Conditional types pour les stratégies de diagnostic
Utilise des conditional types pour déterminer automatiquement la procédure de diagnostic en fonction de l'espèce du dinosaure et de son état de santé. Le système doit « savoir » au niveau des types quels tests effectuer.
1// Species types
2type CarnivoreSpecies = 'TRex' | 'Velociraptor' | 'Spinosaurus' | 'Dilophosaurus';
3type HerbivoreSpecies = 'Triceratops' | 'Brachiosaurus' | 'Stegosaurus' | 'Parasaurolophus';
4type FlyingSpecies = 'Pteranodon' | 'Dimorphodon';
5type AquaticSpecies = 'Mosasaurus' | 'Plesiosaurus';
6
7type AllSpecies = CarnivoreSpecies | HerbivoreSpecies | FlyingSpecies | AquaticSpecies;
8
9// Conditional type - diagnostic procedure depends on species
10type DiagnosticProcedure<S extends AllSpecies> =
11 S extends CarnivoreSpecies ? {
12 type: 'carnivore-diagnostic';
13 sedationRequired: true;
14 aggressionTest: boolean;
15 teethInspection: boolean;
16 clawMeasurement: boolean;
17 huntingBehaviorAnalysis: boolean;
18 } :
19 S extends HerbivoreSpecies ? {
20 type: 'herbivore-diagnostic';
21 sedationRequired: false;
22 digestiveAnalysis: boolean;
23 hoofInspection: boolean;
24 grazingPatternCheck: boolean;
25 } :
26 S extends FlyingSpecies ? {
27 type: 'flying-diagnostic';
28 sedationRequired: true;
29 wingspanMeasurement: number;
30 flightCapabilityTest: boolean;
31 nestInspection: boolean;
32 } :
33 S extends AquaticSpecies ? {
34 type: 'aquatic-diagnostic';
35 sedationRequired: true;
36 underwaterExam: boolean;
37 gillFunction: boolean;
38 divingDepthTest: number;
39 } :
40 never;
41
42// Diagnostic result - depends on the procedure
43type DiagnosticResult<S extends AllSpecies> = {
44 species: S;
45 procedure: DiagnosticProcedure<S>;
46 timestamp: Date;
47 veterinarian: string;
48 passed: boolean;
49 notes: string;
50};
51
52// Usage - TypeScript enforces the correct fields
53const trexDiagnostic: DiagnosticResult<'TRex'> = {
54 species: 'TRex',
55 procedure: {
56 type: 'carnivore-diagnostic',
57 sedationRequired: true,
58 aggressionTest: true,
59 teethInspection: true,
60 clawMeasurement: true,
61 huntingBehaviorAnalysis: false
62 },
63 timestamp: new Date(),
64 veterinarian: 'Dr. Harding',
65 passed: true,
66 notes: 'T-Rex in excellent condition. Sedation went smoothly.'
67};
68
69// Conditional type with infer - extract the procedure type
70type ExtractProcedureType<T> =
71 T extends DiagnosticResult<infer S>
72 ? DiagnosticProcedure<S>
73 : never;
74
75// Distributive conditional type - filtering species requiring sedation
76type RequiresSedation<S extends AllSpecies> =
77 DiagnosticProcedure<S> extends { sedationRequired: true } ? S : never;
78
79type SedatedSpecies = RequiresSedation<AllSpecies>;
80// Result: 'TRex' | 'Velociraptor' | ... (all requiring sedation)3. Template literal types pour une API type-safe
Crée un système d'API qui garantit, au niveau des types, la cohérence des noms d'endpoints, des clés de configuration et des messages de diagnostic.
1// Template literal types for API endpoints
2type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
3type ApiVersion = 'v1' | 'v2';
4type Resource = 'dinosaurs' | 'enclosures' | 'diagnostics' | 'alerts';
5
6// Automatic API path generation
7type ApiEndpoint = `/api/${ApiVersion}/${Resource}`;
8// Result: "/api/v1/dinosaurs" | "/api/v1/enclosures" | ... (12 combinations)
9
10type ApiEndpointWithId = `${ApiEndpoint}/${string}`;
11
12// Template literal types for configuration keys
13type SensorType = 'temperature' | 'heartRate' | 'movement' | 'stress';
14type SensorLocation = 'head' | 'body' | 'tail' | 'legs';
15
16type SensorConfigKey = `sensor_${SensorType}_${SensorLocation}`;
17// Result: "sensor_temperature_head" | "sensor_temperature_body" | ... (16 combinations)
18
19type SensorConfig = Record<SensorConfigKey, {
20 enabled: boolean;
21 threshold: number;
22 interval: number;
23}>;
24
25// Template literal types for diagnostic messages
26type DiagnosticStatus = 'started' | 'completed' | 'failed' | 'cancelled';
27type DiagnosticMessage<S extends AllSpecies, D extends DiagnosticStatus> =
28 `[DIAGNOSTIC] Species: ${S} | Status: ${D}`;
29
30// Usage
31const message: DiagnosticMessage<'TRex', 'completed'> =
32 '[DIAGNOSTIC] Species: TRex | Status: completed';
33
34// Extraction from template literal type
35type ExtractSpecies<T> = T extends `[DIAGNOSTIC] Species: ${infer S} | Status: ${string}`
36 ? S
37 : never;
38
39type ExtractedSpecies = ExtractSpecies<typeof message>; // "TRex"
40
41// Advanced: converting camelCase to kebab-case
42type CamelToKebab<S extends string> =
43 S extends `${infer First}${infer Rest}`
44 ? Rest extends Uncapitalize<Rest>
45 ? `${Lowercase<First>}${CamelToKebab<Rest>}`
46 : `${Lowercase<First>}-${CamelToKebab<Rest>}`
47 : S;
48
49type TestKebab = CamelToKebab<'healthStatus'>; // "health-status"
50type TestKebab2 = CamelToKebab<'dangerRating'>; // "danger-rating"4. Mapped types pour la transformation et la génération de types
Utilise des mapped types pour générer automatiquement des formulaires, des validateurs et des transformations de données de diagnostic.
1// Mapped type: generate form type from data model
2type FormFields<T> = {
3 [K in keyof T]: {
4 value: T[K];
5 label: string;
6 required: boolean;
7 validate: (value: T[K]) => boolean;
8 };
9};
10
11type DinosaurForm = FormFields<Pick<DinosaurRecord, 'name' | 'species' | 'weight' | 'healthStatus'>>;
12
13// Mapped type: generate validator type
14type Validators<T> = {
15 [K in keyof T as `validate${Capitalize<K & string>}`]: (value: T[K]) => boolean;
16};
17
18type DinosaurValidators = Validators<Pick<DinosaurRecord, 'name' | 'age' | 'weight'>>;
19// Result: {
20// validateName: (value: string) => boolean;
21// validateAge: (value: number) => boolean;
22// validateWeight: (value: number) => boolean;
23// }
24
25// Mapped type: generate getters and setters
26type Accessors<T> = {
27 [K in keyof T as `get${Capitalize<K & string>}`]: () => T[K];
28} & {
29 [K in keyof T as `set${Capitalize<K & string>}`]: (value: T[K]) => void;
30};
31
32type DinosaurAccessors = Accessors<Pick<DinosaurRecord, 'name' | 'healthStatus'>>;
33// Result: {
34// getName: () => string;
35// setName: (value: string) => void;
36// getHealthStatus: () => 'healthy' | 'sick' | 'critical' | 'quarantined';
37// setHealthStatus: (value: ...) => void;
38// }
39
40// Mapped type: convert values to Observable
41type Observable<T> = {
42 [K in keyof T]: {
43 value: T[K];
44 subscribe: (callback: (newValue: T[K], oldValue: T[K]) => void) => void;
45 unsubscribe: (callback: Function) => void;
46 };
47};
48
49type ObservableDinosaurMetrics = Observable<DinosaurMetrics>;
50
51// Mapped type implementation at runtime
52function createValidators<T extends Record<string, any>>(
53 rules: { [K in keyof T]: (value: T[K]) => boolean }
54): Validators<T> {
55 const validators = {} as any;
56 for (const key in rules) {
57 const capitalizedKey = key.charAt(0).toUpperCase() + key.slice(1);
58 validators[`validate${capitalizedKey}`] = rules[key];
59 }
60 return validators;
61}
62
63// Usage example
64const dinoValidators = createValidators<Pick<DinosaurRecord, 'name' | 'age' | 'weight'>>({
65 name: (value) => value.length >= 2 && value.length <= 50,
66 age: (value) => value > 0 && value < 200,
67 weight: (value) => value > 0 && value < 100000
68});5. Module augmentation et Test-Driven Development
Étends des types existants via le module augmentation et écris le système selon une approche TDD - les tests d'abord, puis l'implémentation.
1// Module augmentation - extending an existing module
2declare module './dinosaur-record' {
3 interface DinosaurRecord {
4 lastDiagnosticId?: string;
5 diagnosticHistory: DiagnosticEntry[];
6 riskScore: number;
7 }
8}
9
10interface DiagnosticEntry {
11 id: string;
12 date: Date;
13 type: string;
14 result: 'pass' | 'fail' | 'inconclusive';
15 veterinarian: string;
16}
17
18// TDD - write tests first
19// Test 1: DiagnosticCenter should calculate risk
20function testCalculateRiskScore() {
21 const center = new DiagnosticCenter();
22
23 // Healthy herbivore - low risk
24 const herbivoreRisk = center.calculateRiskScore({
25 diet: 'herbivore',
26 healthStatus: 'healthy',
27 dangerRating: 1,
28 age: 5
29 } as DinosaurRecord);
30
31 console.assert(herbivoreRisk < 30,
32 `Expected risk < 30, got ${herbivoreRisk}`);
33
34 // Sick carnivore - high risk
35 const carnivoreRisk = center.calculateRiskScore({
36 diet: 'carnivore',
37 healthStatus: 'sick',
38 dangerRating: 5,
39 age: 12
40 } as DinosaurRecord);
41
42 console.assert(carnivoreRisk > 70,
43 `Expected risk > 70, got ${carnivoreRisk}`);
44
45 console.log('testCalculateRiskScore: PASSED');
46}
47
48// Test 2: System should filter dinosaurs requiring checkup
49function testFilterDinosRequiringCheckup() {
50 const center = new DiagnosticCenter();
51
52 const dinosaurs: Partial<DinosaurRecord>[] = [
53 { name: 'Rex', lastCheckup: new Date('2024-01-01'), healthStatus: 'healthy' },
54 { name: 'Blue', lastCheckup: new Date('2025-12-01'), healthStatus: 'healthy' },
55 { name: 'Echo', lastCheckup: new Date('2025-12-15'), healthStatus: 'critical' }
56 ];
57
58 const needCheckup = center.filterRequiringCheckup(
59 dinosaurs as DinosaurRecord[],
60 30 // Days since last checkup
61 );
62
63 // Rex - checkup more than 30 days ago
64 // Echo - "critical" status requires immediate checkup
65 console.assert(needCheckup.length === 2,
66 `Expected 2 dinosaurs, got ${needCheckup.length}`);
67
68 console.log('testFilterDinosRequiringCheckup: PASSED');
69}
70
71// Test 3: Report generation should return the correct format
72function testGenerateReport() {
73 const center = new DiagnosticCenter();
74
75 const report = center.generateReport('TRex', {
76 species: 'TRex',
77 procedure: {
78 type: 'carnivore-diagnostic',
79 sedationRequired: true,
80 aggressionTest: true,
81 teethInspection: true,
82 clawMeasurement: true,
83 huntingBehaviorAnalysis: false
84 },
85 timestamp: new Date(),
86 veterinarian: 'Dr. Harding',
87 passed: true,
88 notes: 'Examination completed successfully'
89 });
90
91 console.assert(report.includes('[DIAGNOSTIC]'),
92 'Report should contain [DIAGNOSTIC] header');
93 console.assert(report.includes('TRex'),
94 'Report should contain species');
95 console.assert(report.includes('Dr. Harding'),
96 'Report should contain veterinarian');
97
98 console.log('testGenerateReport: PASSED');
99}
100
101// Implementation (after writing tests)
102class DiagnosticCenter {
103 calculateRiskScore(dino: Pick<DinosaurRecord, 'diet' | 'healthStatus' | 'dangerRating' | 'age'>): number {
104 let score = 0;
105
106 // Diet affects base risk
107 const dietScores = { carnivore: 30, omnivore: 15, herbivore: 5 };
108 score += dietScores[dino.diet];
109
110 // Health status
111 const healthScores = { healthy: 0, sick: 20, critical: 40, quarantined: 30 };
112 score += healthScores[dino.healthStatus];
113
114 // Danger level (1-5) * 6
115 score += dino.dangerRating * 6;
116
117 // Age - older dinosaurs are more unpredictable
118 if (dino.age > 10) score += 10;
119
120 return Math.min(100, Math.max(0, score));
121 }
122
123 filterRequiringCheckup(dinosaurs: DinosaurRecord[], maxDaysSinceCheckup: number): DinosaurRecord[] {
124 const now = new Date();
125 return dinosaurs.filter(dino => {
126 // Critical status - always requires checkup
127 if (dino.healthStatus === 'critical') return true;
128
129 // Check time since last checkup
130 const daysSinceCheckup = Math.floor(
131 (now.getTime() - dino.lastCheckup.getTime()) / (1000 * 60 * 60 * 24)
132 );
133 return daysSinceCheckup > maxDaysSinceCheckup;
134 });
135 }
136
137 generateReport<S extends AllSpecies>(
138 species: S,
139 result: DiagnosticResult<S>
140 ): string {
141 return [
142 `[DIAGNOSTIC] Species: ${species} | Status: ${result.passed ? 'completed' : 'failed'}`,
143 `Veterinarian: ${result.veterinarian}`,
144 `Date: ${result.timestamp.toISOString()}`,
145 `Result: ${result.passed ? 'POSITIVE' : 'NEGATIVE'}`,
146 `Notes: ${result.notes}`
147 ].join('\n');
148 }
149}
150
151// Running tests
152testCalculateRiskScore();
153testFilterDinosRequiringCheckup();
154testGenerateReport();Conseils d'implémentation
Commence par définir les types et interfaces de base - c'est le socle de ton système de diagnostic. Construis ensuite les conditional types pour les procédures de diagnostic, les template literal types pour l'API, et les mapped types pour les transformations. Enfin, écris les tests et implémente le DiagnosticCenter.
Ordre d'implémentation suggéré :
1// 1. Base types: DinosaurRecord, AllSpecies
2// 2. Utility types: DeepReadonly, RequireAtLeastOne, NumericFields
3// 3. Conditional types: DiagnosticProcedure<S>, DiagnosticResult<S>
4// 4. Template literal types: ApiEndpoint, SensorConfigKey
5// 5. Mapped types: FormFields, Validators, Accessors
6// 6. Module augmentation: extending DinosaurRecord
7// 7. Unit tests (TDD)
8// 8. DiagnosticCenter implementation
9// 9. Integration and running testsSouviens-toi des principes clés :
- Les utility types servent à transformer des types existants - ne crée pas de nouveaux types de zéro quand tu peux en transformer des existants
- Les conditional types avec
inferpermettent d'extraire des types depuis des structures imbriquées - Les template literal types garantissent la cohérence des chaînes au moment de la compilation
- Les mapped types avec
aspermettent de renommer les clés - Le module augmentation nécessite
declare moduleet fusionne avec le type d'origine - Le TDD signifie : écrire le test -> exécuter (rouge) -> implémenter -> exécuter (vert) -> refactoriser
Bonne chance !
« Souviens-toi », dit le Dr Sattler avec un sourire, « à Jurassic Park il n'y a pas de place pour les erreurs de type. Chaque any est une faille de sécurité potentielle, chaque type manquant est une porte d'enclos ouverte. TypeScript te donne les outils pour attraper 90 % des bugs avant même que le code ne s'exécute. Utility types, conditional types, mapped types, template literal types - voilà ton arsenal. Utilise-le avec sagesse. »
« Et les tests ? Les tests, c'est comme les systèmes d'alarme des enclos. On ne les construit pas après l'évasion du T-Rex. On les construit AVANT que cela n'arrive. Le TDD est une philosophie qui protège ton code de la catastrophe. »
Ouvre ton éditeur et construis ton système de diagnostic - complet, entièrement typé et testé !
Code de cette leçon : index.ts
1// Projet final : système de diagnostic de Jurassic Park
2// Combine : utility types, conditional types, mapped types, template literal types, TDD
3
4// === 1. UTILITY TYPES AVANCÉS ===
5interface DinosaurRecord {
6 id: string;
7 species: string;
8 name: string;
9 age: number;
10 weight: number;
11 diet: "carnivore" | "herbivore" | "omnivore";
12 healthStatus: "healthy" | "sick" | "critical";
13 enclosureId: string;
14 dangerRating: 1 | 2 | 3 | 4 | 5;
15}
16
17// TODO : Crée DeepReadonly<T> - readonly récursif
18type DeepReadonly<T> = {
19 // TODO : Utilise des mapped types avec condition pour les objets imbriqués
20 readonly [K in keyof T]: T[K];
21};
22
23// TODO : Crée RequireAtLeastOne<T> - exige au moins un champ
24// TODO : Crée Mutable<T> - retire readonly des champs
25
26// === 2. CONDITIONAL TYPES ===
27// TODO : Crée le type DiagnosticStrategy<T>
28// Si diet = "carnivore" -> exige : bitePressureTest, aggressionLevel
29// Si diet = "herbivore" -> exige : foliagePreference, migrationPattern
30type DiagnosticStrategy<T extends DinosaurRecord["diet"]> =
31 // TODO : Implémente la condition
32 T extends "carnivore" ? { bitePressureTest: number; aggressionLevel: number }
33 : { foliagePreference: string[]; migrationPattern: string };
34
35// === 3. TEMPLATE LITERAL TYPES ===
36// TODO : Crée des endpoints d'API type-safe
37type Endpoint = "dinosaurs" | "enclosures" | "alerts";
38type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
39
40// TODO : Crée le type ApiRoute = "${HttpMethod} /api/${Endpoint}"
41// TODO : Crée la fonction handleRequest(route: ApiRoute): void
42
43// === 4. MAPPED TYPES POUR LE DIAGNOSTIC ===
44// TODO : Crée le type DiagnosticForm<T> qui transforme tous les champs en champs de formulaire
45// ex. { name: string } -> { name: { value: string; isValid: boolean; error?: string } }
46
47// === 5. SYSTÈME DE TEST SIMPLE (simulation TDD) ===
48// TODO : Crée des fonctions de test qui vérifient la validité du système
49function assertEqual<T>(actual: T, expected: T, testName: string): void {
50 const passed = JSON.stringify(actual) === JSON.stringify(expected);
51 console.log(passed ? `PASS: ${testName}` : `FAIL: ${testName}`);
52 if (!passed) {
53 console.log(` Expected: ${JSON.stringify(expected)}`);
54 console.log(` Actual: ${JSON.stringify(actual)}`);
55 }
56}
57
58// TODO : Écris les tests du système de diagnostic
59// Test 1 : Enregistrement d'un dinosaure
60// Test 2 : Diagnostic carnivore vs herbivore
61// Test 3 : Génération du rapport
62// Test 4 : Validation des données d'entrée
63
64console.log("Système de diagnostic - lance les tests !");Tu as repéré une erreur dans cette leçon ?