Cours JavaScript et TypeScript · Module 10: TypeScript en pratique
TypeScript avec les frameworks populaires
Dans cette leçon5
« Bon retour à Jurassic Park », dit le Dr Rex. « Tu connais déjà TypeScript en tant que langage autonome, mais dans le monde réel, on l'utilise rarement à l'état pur. En général, on le combine avec des frameworks qui aident à construire des applications complexes. » Sans types, une faute de frappe dans le nom d'une prop ne se révélera qu'au moment où la fiche du vélociraptor affichera « Sans danger ».
TypeScript avec React - des composants typés
React est l'une des bibliothèques les plus populaires pour construire des interfaces utilisateur. TypeScript s'y intègre parfaitement, en assurant un typage fort des composants, des props et des hooks.
Les composants fonctionnels de base
Tu décris les props avec une interface, dans laquelle le signe ? marque un champ optionnel, et le composant reçoit le type React.FC<DinoCardProps> :
1// Interface pour les props du composant
2interface DinoCardProps {
3 name: string;
4 species: string;
5 isDangerous: boolean;
6 age?: number; // Champ optionnel
7 onEscape?: () => void; // Fonction callback optionnelle
8}
9
10// Composant fonctionnel avec des props typées
11const DinoCard: React.FC<DinoCardProps> = ({
12 name,
13 species,
14 isDangerous,
15 age,
16 onEscape
17}) => {
18 return (
19 <div className="dino-card">
20 <h2>{name}</h2>
21 <p>Espèce : {species}</p>
22 <p>Âge : {age || 'Inconnu'}</p>
23 <div className={isDangerous ? 'danger' : 'safe'}>
24 {isDangerous ? 'DANGEREUX' : 'Sans danger'}
25 </div>
26 {onEscape && (
27 <button onClick={onEscape}>
28 Signaler une évasion
29 </button>
30 )}
31 </div>
32 );
33};
34
35// Utilisation avec un typage complet
36const App = () => {
37 const handleEscape = () => {
38 console.log("ALARME : un dinosaure s'est échappé !");
39 };
40
41 return (
42 <DinoCard
43 name="Blue"
44 species="Velociraptor"
45 isDangerous={true}
46 age={4}
47 onEscape={handleEscape}
48 />
49 );
50};L'absence de la prop species provoque une erreur de compilation dès App. Beaucoup d'équipes écrivent simplement function DinoCard(props: DinoCardProps) au lieu de React.FC, car depuis React 18, les deux écritures fonctionnent pratiquement de la même façon. Pour les nombres, ?? vaut mieux que ||, car un âge de 0 est lui aussi valide.
Typer les hooks React
Les hooks sont génériques. Pour des valeurs simples, TypeScript déduit le type tout seul, mais pour un tableau vide ou null, tu l'indiques entre chevrons :
1import { useState, useEffect, useRef, useCallback, useMemo } from 'react';
2
3interface Dinosaur {
4 id: number;
5 name: string;
6 species: string;
7 status: 'contained' | 'escaped' | 'feeding';
8}
9
10const DinosaurMonitor: React.FC = () => {
11 // useState avec un type
12 const [dinosaurs, setDinosaurs] = useState<Dinosaur[]>([]);
13 const [loading, setLoading] = useState<boolean>(false);
14 const [error, setError] = useState<Error | null>(null);
15
16 // useRef avec le type de l'élément DOM
17 const alertSound = useRef<HTMLAudioElement>(null);
18
19 // useEffect - récupération des données
20 useEffect(() => {
21 const fetchDinosaurs = async () => {
22 setLoading(true);
23 try {
24 const response = await fetch('/api/dinosaurs');
25 const data: Dinosaur[] = await response.json();
26 setDinosaurs(data);
27 } catch (err) {
28 setError(err as Error);
29 } finally {
30 setLoading(false);
31 }
32 };
33
34 fetchDinosaurs();
35 }, []);
36
37 // useCallback avec des paramètres typés
38 const handleDinoEscape = useCallback((dinoId: number) => {
39 setDinosaurs(prev =>
40 prev.map(dino =>
41 dino.id === dinoId
42 ? { ...dino, status: 'escaped' as const }
43 : dino
44 )
45 );
46
47 // Jouer l'alarme
48 if (alertSound.current) {
49 alertSound.current.play();
50 }
51 }, []);
52
53 // useMemo avec un type de retour
54 const dangerousDinos = useMemo<Dinosaur[]>(() => {
55 return dinosaurs.filter(dino => dino.status === 'escaped');
56 }, [dinosaurs]);
57
58 if (loading) return <div>Chargement des données de surveillance...</div>;
59 if (error) return <div>Erreur : {error.message}</div>;
60
61 return (
62 <div>
63 <h1>Moniteur des dinosaures</h1>
64 <audio ref={alertSound} src="/sounds/alarm.mp3" />
65
66 {dangerousDinos.length > 0 && (
67 <div className="alert">
68 ATTENTION : {dangerousDinos.length} dinosaures en liberté !
69 </div>
70 )}
71
72 <div className="dino-grid">
73 {dinosaurs.map(dino => (
74 <DinoCard
75 key={dino.id}
76 name={dino.name}
77 species={dino.species}
78 isDangerous={dino.status === 'escaped'}
79 onEscape={() => handleDinoEscape(dino.id)}
80 />
81 ))}
82 </div>
83 </div>
84 );
85};Sans <Dinosaur[]>, le tableau vide aurait le type never[] et on ne pourrait rien y ajouter. useRef<HTMLAudioElement>(null) sait que current est un élément audio ou null, donc le code le vérifie avant play(), et as const conserve le littéral 'escaped' au lieu de l'élargir en string.
Custom hooks avec TypeScript
Un hook maison renvoie un objet décrit par une interface, donc le composant bénéficie de l'autocomplétion pour chaque fonction. Omit<Dinosaur, 'id'> désigne un dinosaure sans id, que le serveur attribuera :
1// Custom hook pour gérer une liste de dinosaures
2interface UseDinosaursResult {
3 dinosaurs: Dinosaur[];
4 loading: boolean;
5 error: Error | null;
6 addDinosaur: (dino: Omit<Dinosaur, 'id'>) => Promise<void>;
7 removeDinosaur: (id: number) => Promise<void>;
8 updateStatus: (id: number, status: Dinosaur['status']) => void;
9}
10
11function useDinosaurs(): UseDinosaursResult {
12 const [dinosaurs, setDinosaurs] = useState<Dinosaur[]>([]);
13 const [loading, setLoading] = useState<boolean>(false);
14 const [error, setError] = useState<Error | null>(null);
15
16 const addDinosaur = useCallback(async (dino: Omit<Dinosaur, 'id'>) => {
17 setLoading(true);
18 try {
19 const response = await fetch('/api/dinosaurs', {
20 method: 'POST',
21 headers: { 'Content-Type': 'application/json' },
22 body: JSON.stringify(dino)
23 });
24 const newDino: Dinosaur = await response.json();
25 setDinosaurs(prev => [...prev, newDino]);
26 } catch (err) {
27 setError(err as Error);
28 } finally {
29 setLoading(false);
30 }
31 }, []);
32
33 const removeDinosaur = useCallback(async (id: number) => {
34 setLoading(true);
35 try {
36 await fetch(`/api/dinosaurs/${id}`, { method: 'DELETE' });
37 setDinosaurs(prev => prev.filter(d => d.id !== id));
38 } catch (err) {
39 setError(err as Error);
40 } finally {
41 setLoading(false);
42 }
43 }, []);
44
45 const updateStatus = useCallback((id: number, status: Dinosaur['status']) => {
46 setDinosaurs(prev =>
47 prev.map(dino =>
48 dino.id === id ? { ...dino, status } : dino
49 )
50 );
51 }, []);
52
53 return {
54 dinosaurs,
55 loading,
56 error,
57 addDinosaur,
58 removeDinosaur,
59 updateStatus
60 };
61}
62
63// Utilisation du custom hook
64const ParkManager: React.FC = () => {
65 const {
66 dinosaurs,
67 loading,
68 error,
69 addDinosaur,
70 updateStatus
71 } = useDinosaurs();
72
73 const handleFeedDino = (id: number) => {
74 updateStatus(id, 'feeding');
75 setTimeout(() => updateStatus(id, 'contained'), 5000);
76 };
77
78 return (
79 <div>
80 {/* Composant qui utilise le hook */}
81 </div>
82 );
83};Dinosaur['status'] garantit que updateStatus n'accepte que l'un des trois statuts.
TypeScript avec Node.js/Express - une API backend
Typer une application Express
Côté serveur, tu types les requêtes, les réponses et les middlewares, et un contrôleur regroupe les handlers d'une même ressource :
1import express, { Request, Response, NextFunction } from 'express';
2
3// Interfaces pour les modèles de données
4interface Dinosaur {
5 id: number;
6 name: string;
7 species: string;
8 paddock: number;
9 feedingSchedule: string[];
10}
11
12interface CreateDinosaurDTO {
13 name: string;
14 species: string;
15 paddock: number;
16}
17
18// Extension de Request avec des champs personnalisés
19interface AuthRequest extends Request {
20 user?: {
21 id: number;
22 role: 'admin' | 'keeper' | 'visitor';
23 };
24}
25
26// Contrôleur typé
27class DinosaurController {
28 // GET /api/dinosaurs
29 async getAll(req: Request, res: Response<Dinosaur[]>, next: NextFunction) {
30 try {
31 const dinosaurs = await this.dinosaurService.findAll();
32 res.json(dinosaurs);
33 } catch (error) {
34 next(error);
35 }
36 }
37
38 // GET /api/dinosaurs/:id
39 async getById(
40 req: Request<{ id: string }>,
41 res: Response<Dinosaur>,
42 next: NextFunction
43 ) {
44 try {
45 const id = parseInt(req.params.id);
46 const dinosaur = await this.dinosaurService.findById(id);
47
48 if (!dinosaur) {
49 res.status(404).json({ error: 'Dinosaur not found' } as any);
50 return;
51 }
52
53 res.json(dinosaur);
54 } catch (error) {
55 next(error);
56 }
57 }
58
59 // POST /api/dinosaurs
60 async create(
61 req: Request<{}, {}, CreateDinosaurDTO>,
62 res: Response<Dinosaur>,
63 next: NextFunction
64 ) {
65 try {
66 const newDino = await this.dinosaurService.create(req.body);
67 res.status(201).json(newDino);
68 } catch (error) {
69 next(error);
70 }
71 }
72}
73
74// Middleware typé
75const authenticateUser = (
76 req: AuthRequest,
77 res: Response,
78 next: NextFunction
79): void => {
80 const token = req.headers.authorization?.split(' ')[1];
81
82 if (!token) {
83 res.status(401).json({ error: 'No token provided' });
84 return;
85 }
86
87 try {
88 const decoded = verifyToken(token);
89 req.user = decoded;
90 next();
91 } catch (error) {
92 res.status(401).json({ error: 'Invalid token' });
93 }
94};
95
96// Routing
97const app = express();
98
99app.use(express.json());
100
101const dinoController = new DinosaurController();
102
103app.get('/api/dinosaurs', dinoController.getAll.bind(dinoController));
104app.get('/api/dinosaurs/:id', dinoController.getById.bind(dinoController));
105app.post('/api/dinosaurs', authenticateUser, dinoController.create.bind(dinoController));bind(dinoController) est indispensable, car une méthode passée comme callback perd son this. Il faut encore déclarer le champ this.dinosaurService et l'injecter par le constructeur, et au lieu de l'interface AuthRequest, tu peux utiliser l'augmentation de express-serve-static-core vue dans la leçon précédente.
Des services et des repositories typés
Le pattern Repository cache la base de données derrière une interface générique Repository<T>, et le service contient la logique métier, comme la validation :
1// Repository Pattern avec TypeScript
2interface Repository<T> {
3 findAll(): Promise<T[]>;
4 findById(id: number): Promise<T | null>;
5 create(data: Omit<T, 'id'>): Promise<T>;
6 update(id: number, data: Partial<T>): Promise<T>;
7 delete(id: number): Promise<void>;
8}
9
10class DinosaurRepository implements Repository<Dinosaur> {
11 async findAll(): Promise<Dinosaur[]> {
12 // Implémentation avec une base de données
13 return [];
14 }
15
16 async findById(id: number): Promise<Dinosaur | null> {
17 // Implémentation
18 return null;
19 }
20
21 async create(data: Omit<Dinosaur, 'id'>): Promise<Dinosaur> {
22 // Implémentation
23 return { id: 1, ...data } as Dinosaur;
24 }
25
26 async update(id: number, data: Partial<Dinosaur>): Promise<Dinosaur> {
27 // Implémentation
28 return {} as Dinosaur;
29 }
30
31 async delete(id: number): Promise<void> {
32 // Implémentation
33 }
34}
35
36// Service Layer
37class DinosaurService {
38 constructor(private repository: DinosaurRepository) {}
39
40 async getAllDinosaurs(): Promise<Dinosaur[]> {
41 return this.repository.findAll();
42 }
43
44 async getDinosaurById(id: number): Promise<Dinosaur> {
45 const dino = await this.repository.findById(id);
46 if (!dino) {
47 throw new Error(`Dinosaur with id ${id} not found`);
48 }
49 return dino;
50 }
51
52 async createDinosaur(data: CreateDinosaurDTO): Promise<Dinosaur> {
53 // Validation
54 this.validateDinosaurData(data);
55
56 // Création
57 return this.repository.create({
58 ...data,
59 feedingSchedule: this.generateFeedingSchedule(data.species)
60 });
61 }
62
63 private validateDinosaurData(data: CreateDinosaurDTO): void {
64 if (!data.name || data.name.length < 2) {
65 throw new Error('Invalid dinosaur name');
66 }
67 if (data.paddock < 1 || data.paddock > 10) {
68 throw new Error('Invalid paddock number');
69 }
70 }
71
72 private generateFeedingSchedule(species: string): string[] {
73 // Logique de génération du planning
74 return ['08:00', '14:00', '20:00'];
75 }
76}Le service dépend de la classe concrète DinosaurRepository, alors que selon le principe d'inversion des dépendances, il vaudrait mieux accepter un Repository<Dinosaur> et fournir dans les tests un repository en mémoire. Partial<T> n'agit que sur le premier niveau, donc pour les objets imbriqués, on écrit un DeepPartial<T> récursif, et le type intégré NonNullable<T> retire null et undefined d'un type.
TypeScript avec Next.js - le full-stack avec un support natif
Next.js prend en charge TypeScript nativement : il suffit de créer un fichier .ts ou .tsx, et Next.js configure lui-même le compilateur et crée le tsconfig.json.
Pages et API Routes
Dans le Pages Router, la page reçoit le type NextPage<Props>, et la fonction qui récupère les données le type GetServerSideProps<Props>, donc les props de la page et les données du serveur doivent correspondre :
1// pages/dinosaurs/[id].tsx - Dynamic route typée
2import { GetServerSideProps, NextPage } from 'next';
3
4interface DinosaurPageProps {
5 dinosaur: Dinosaur;
6 lastUpdated: string;
7}
8
9// Composant de page
10const DinosaurPage: NextPage<DinosaurPageProps> = ({ dinosaur, lastUpdated }) => {
11 return (
12 <div>
13 <h1>{dinosaur.name}</h1>
14 <p>Espèce : {dinosaur.species}</p>
15 <p>Enclos : {dinosaur.paddock}</p>
16 <small>Dernière mise à jour : {lastUpdated}</small>
17 </div>
18 );
19};
20
21// Server-side rendering typé
22export const getServerSideProps: GetServerSideProps<DinosaurPageProps> = async (context) => {
23 const { id } = context.params as { id: string };
24
25 try {
26 const response = await fetch(`https://api.park.com/dinosaurs/${id}`);
27 const dinosaur: Dinosaur = await response.json();
28
29 return {
30 props: {
31 dinosaur,
32 lastUpdated: new Date().toISOString()
33 }
34 };
35 } catch (error) {
36 return {
37 notFound: true
38 };
39 }
40};
41
42export default DinosaurPage;Si getServerSideProps renvoie un objet sans lastUpdated, le compilateur signalera une erreur avant que la page n'arrive jusqu'aux visiteurs.
Les API Routes dans Next.js
Une route d'API du Pages Router est une fonction typée avec NextApiRequest et NextApiResponse<T>, où T décrit la forme de la réponse :
1// pages/api/dinosaurs/[id].ts
2import type { NextApiRequest, NextApiResponse } from 'next';
3
4type ResponseData = {
5 dinosaur?: Dinosaur;
6 error?: string;
7};
8
9export default async function handler(
10 req: NextApiRequest,
11 res: NextApiResponse<ResponseData>
12) {
13 const { id } = req.query;
14
15 if (req.method === 'GET') {
16 try {
17 const dinosaur = await getDinosaurById(Number(id));
18 res.status(200).json({ dinosaur });
19 } catch (error) {
20 res.status(404).json({ error: 'Dinosaur not found' });
21 }
22 } else if (req.method === 'PUT') {
23 try {
24 const updatedDino = await updateDinosaur(Number(id), req.body);
25 res.status(200).json({ dinosaur: updatedDino });
26 } catch (error) {
27 res.status(400).json({ error: 'Update failed' });
28 }
29 } else {
30 res.status(405).json({ error: 'Method not allowed' });
31 }
32}req.query.id a le type string | string[] | undefined, c'est pourquoi le code le convertit en nombre, et une méthode non prise en charge reçoit le statut 405.
App Router (Next.js 13+) avec les Server Components
Dans l'App Router, une page est par défaut un Server Component, qui peut être une fonction async et récupérer les données directement. Tu extrais les parties interactives dans des Client Components :
1// app/dinosaurs/[id]/page.tsx
2// Depuis Next.js 15, params et searchParams sont des objets Promise
3interface PageProps {
4 params: Promise<{ id: string }>;
5 searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
6}
7
8// Server Component
9export default async function DinosaurDetailPage({ params }: PageProps) {
10 const { id } = await params;
11 const dinosaur = await getDinosaurById(id);
12
13 return (
14 <div>
15 <h1>{dinosaur.name}</h1>
16 <DinosaurDetails dino={dinosaur} />
17 </div>
18 );
19}
20
21// Client Component - un fichier séparé, par exemple app/dinosaurs/[id]/DinosaurDetails.tsx,
22// car la directive 'use client' doit figurer tout en haut du fichier
23'use client';
24
25interface DinosaurDetailsProps {
26 dino: Dinosaur;
27}
28
29export function DinosaurDetails({ dino }: DinosaurDetailsProps) {
30 const [isFavorite, setIsFavorite] = useState(false);
31
32 return (
33 <div>
34 <button onClick={() => setIsFavorite(!isFavorite)}>
35 {isFavorite ? '★' : '☆'} Favori
36 </button>
37 {/* Reste du composant */}
38 </div>
39 );
40}Depuis Next.js 15, params est un objet Promise, donc tu dois d'abord l'attendre, alors qu'en versions 13 et 14, c'était un objet ordinaire. La directive 'use client' doit figurer au tout début d'un fichier séparé, c'est pourquoi DinosaurDetails vit dans son propre module.
Les patterns de typage dans les frameworks
Generic Components
Un composant générique fonctionne pour n'importe quel type d'éléments, et TypeScript veille à ce que renderItem et keyExtractor correspondent au tableau transmis :
1// Composant de liste universel et typé
2interface ListProps<T> {
3 items: T[];
4 renderItem: (item: T, index: number) => React.ReactNode;
5 keyExtractor: (item: T) => string | number;
6 emptyMessage?: string;
7}
8
9function List<T>({ items, renderItem, keyExtractor, emptyMessage }: ListProps<T>) {
10 if (items.length === 0) {
11 return <div>{emptyMessage || 'Aucun élément'}</div>;
12 }
13
14 return (
15 <ul>
16 {items.map((item, index) => (
17 <li key={keyExtractor(item)}>
18 {renderItem(item, index)}
19 </li>
20 ))}
21 </ul>
22 );
23}
24
25// Utilisation
26const DinosaurList = () => {
27 const dinosaurs: Dinosaur[] = [
28 { id: 1, name: 'Rex', species: 'T-Rex', paddock: 1, feedingSchedule: [] },
29 { id: 2, name: 'Blue', species: 'Velociraptor', paddock: 3, feedingSchedule: [] }
30 ];
31
32 return (
33 <List<Dinosaur>
34 items={dinosaurs}
35 keyExtractor={(dino) => dino.id}
36 renderItem={(dino) => (
37 <div>
38 <strong>{dino.name}</strong> - {dino.species}
39 </div>
40 )}
41 emptyMessage="Aucun dinosaure dans le système"
42 />
43 );
44};L'écriture List<Dinosaur> dans le JSX indique le type explicitement, même si TypeScript le déduirait de items, et ce même composant affichera aussi une liste d'enclos.
Form handling avec TypeScript
La bibliothèque react-hook-form accepte le type des données du formulaire, donc register n'accepte que des noms de champs existants :
1import { useForm, SubmitHandler } from 'react-hook-form';
2
3interface DinosaurFormData {
4 name: string;
5 species: string;
6 paddock: number;
7 isDangerous: boolean;
8 feedingTimes: string[];
9}
10
11const DinosaurForm: React.FC = () => {
12 const {
13 register,
14 handleSubmit,
15 formState: { errors }
16 } = useForm<DinosaurFormData>();
17
18 const onSubmit: SubmitHandler<DinosaurFormData> = async (data) => {
19 try {
20 const response = await fetch('/api/dinosaurs', {
21 method: 'POST',
22 headers: { 'Content-Type': 'application/json' },
23 body: JSON.stringify(data)
24 });
25
26 if (response.ok) {
27 alert('Dinosaure ajouté !');
28 }
29 } catch (error) {
30 console.error("Erreur lors de l'ajout :", error);
31 }
32 };
33
34 return (
35 <form onSubmit={handleSubmit(onSubmit)}>
36 <input
37 {...register('name', { required: 'Le nom est obligatoire' })}
38 placeholder="Nom du dinosaure"
39 />
40 {errors.name && <span>{errors.name.message}</span>}
41
42 <input
43 {...register('species', { required: true })}
44 placeholder="Espèce"
45 />
46
47 <input
48 type="number"
49 {...register('paddock', {
50 required: true,
51 min: 1,
52 max: 10,
53 valueAsNumber: true // sans cela, la valeur d'un champ number est une chaîne
54 })}
55 placeholder="Numéro d'enclos"
56 />
57
58 <label>
59 <input type="checkbox" {...register('isDangerous')} />
60 Espèce dangereuse
61 </label>
62
63 <button type="submit">Ajouter le dinosaure</button>
64 </form>
65 );
66};Une faute de frappe dans le nom d'un champ provoque une erreur de compilation. Un champ type="number" renvoie une chaîne dans le navigateur, c'est pourquoi l'option valueAsNumber la convertit en un nombre conforme au type paddock: number.
Récapitulatif
« Tu vois ? », sourit le Dr Rex. « TypeScript s'intègre parfaitement aux frameworks populaires. »
React
- Un typage fort des composants grâce à
React.FC<Props> - Des hooks typés (
useState<T>,useRef<HTMLElement>) - Des custom hooks avec une type safety complète
- Des generic components pour la réutilisabilité
Node.js/Express
- Le typage de Request, Response, NextFunction
- Les patterns Repository et Service avec des interfaces
- Des middlewares avec des types de Request personnalisés
- Des DTO (Data Transfer Objects) qui décrivent les données d'entrée
Next.js
- Un support natif de TypeScript sans configuration manuelle
- Des
getServerSidePropsetgetStaticPropstypés - Des API Routes typées
- Server et Client Components dans l'App Router
Les principaux avantages : attraper les erreurs dès l'écriture du code, l'autocomplétion (IntelliSense), une refactorisation sans crainte, un code qui se documente lui-même et une collaboration plus facile en équipe. Commence chaque nouveau projet directement en TypeScript avec le mode strict car ajouter des types à une application existante coûte plus cher. Dans la leçon suivante, tu réuniras les connaissances de tout le module dans le projet final : le système de diagnostic du parc.
« TypeScript, c'est la sécurité des systèmes du parc : au début, il demande un peu de travail, mais à long terme, il t'épargne une foule de problèmes », conclut le Dr Rex.
Code de cette leçon : index.js
1// Profilage mémoire - trouve et corrige les memory leaks
2console.log("Jurassic Park - Laboratoire de profilage mémoire");
3console.log("Trouve et corrige les memory leaks dans le code !\n");
4
5// ====================================
6// TÂCHE : Le code ci-dessous comporte plusieurs memory leaks.
7// Corrige-les en appliquant les bonnes pratiques.
8// ====================================
9
10// LEAK 1 : Event listeners sans nettoyage
11// Corrige : Ajoute une méthode destroy() avec removeEventListener
12class DinosaurTracker {
13 constructor(name) {
14 this.name = name;
15 this.positions = [];
16 this.handler = this.onPositionUpdate.bind(this);
17 // Simulation addEventListener
18 console.log(`Tracker pour ${name} - listener ajouté`);
19 }
20
21 onPositionUpdate(position) {
22 this.positions.push(position);
23 }
24
25 // TODO : Ajoute une méthode destroy() qui retire l'event listener
26 destroy() {
27 // Solution : retrait du listener
28 // removeEventListener('position', this.handler);
29 this.positions = [];
30 this.handler = null;
31 console.log(`Tracker pour ${this.name} - listener retiré, mémoire libérée`);
32 }
33}
34
35// LEAK 2 : Tableau qui grandit sans limite
36// Corrige : Limite la taille du tableau
37class AlertSystem {
38 constructor() {
39 this.alerts = [];
40 this.maxAlerts = 100; // TODO : Définis la limite
41 }
42
43 addAlert(message) {
44 // TODO : Vérifie la limite et supprime les alertes les plus anciennes
45 if (this.alerts.length >= this.maxAlerts) {
46 this.alerts.shift(); // Retire le plus ancien
47 }
48 this.alerts.push({
49 message,
50 timestamp: Date.now()
51 });
52 }
53
54 getAlertCount() {
55 return this.alerts.length;
56 }
57}
58
59// LEAK 3 : Timers oubliés
60// Corrige : Ajoute le nettoyage des timers
61class HealthMonitor {
62 constructor() {
63 this.timers = [];
64 this.data = [];
65 }
66
67 startMonitoring(dinoName, intervalMs) {
68 const timer = setInterval(() => {
69 this.data.push({
70 dino: dinoName,
71 health: Math.random() * 100,
72 time: Date.now()
73 });
74 }, intervalMs);
75
76 this.timers.push(timer);
77 console.log(`Surveillance de ${dinoName} démarrée`);
78 }
79
80 // TODO : Ajoute une méthode stopAll() qui nettoie les timers
81 stopAll() {
82 this.timers.forEach(timer => clearInterval(timer));
83 this.timers = [];
84 this.data = [];
85 console.log("Tous les moniteurs arrêtés, timers nettoyés");
86 }
87}
88
89// LEAK 4 : Références circulaires
90// Corrige : Utilise WeakRef ou WeakMap
91class Enclosure {
92 constructor(name) {
93 this.name = name;
94 this.dinosaurs = [];
95 }
96
97 addDinosaur(dino) {
98 this.dinosaurs.push(dino);
99 // Référence circulaire : dino -> enclosure -> dino
100 dino.enclosure = this;
101 }
102}
103
104// Solution avec WeakRef
105class EnclosureFixed {
106 constructor(name) {
107 this.name = name;
108 this.dinosaurs = new Set();
109 }
110
111 addDinosaur(dino) {
112 this.dinosaurs.add(dino);
113 // WeakRef ne bloque pas le garbage collection
114 dino.enclosureRef = new WeakRef(this);
115 }
116
117 removeDinosaur(dino) {
118 this.dinosaurs.delete(dino);
119 dino.enclosureRef = null;
120 }
121}
122
123// ====================================
124// TESTS
125// ====================================
126console.log("=== Test des leaks corrigés ===\n");
127
128// Test 1 : Tracker avec nettoyage
129const tracker = new DinosaurTracker("T-Rex");
130tracker.onPositionUpdate({ x: 10, y: 20 });
131tracker.onPositionUpdate({ x: 15, y: 25 });
132console.log("Positions avant destroy :", tracker.positions.length);
133tracker.destroy();
134console.log("Positions après destroy :", tracker.positions.length);
135
136// Test 2 : Système d'alerte avec limite
137console.log("\n--- Système d'alerte ---");
138const alertSys = new AlertSystem();
139for (let i = 0; i < 150; i++) {
140 alertSys.addAlert(`Alert #${i}`);
141}
142console.log(`Alertes après 150 ajouts (limite 100) : ${alertSys.getAlertCount()}`);
143
144// Test 3 : Health monitor avec nettoyage
145console.log("\n--- Health Monitor ---");
146const healthMon = new HealthMonitor();
147healthMon.startMonitoring("Velociraptor", 1000);
148healthMon.startMonitoring("Stegosaurus", 2000);
149console.log("Timers actifs :", healthMon.timers.length);
150healthMon.stopAll();
151console.log("Timers après stopAll :", healthMon.timers.length);
152
153// Test 4 : Solution avec WeakRef
154console.log("\n--- Enclos avec WeakRef ---");
155const enclosure = new EnclosureFixed("Paddock A");
156let dino = { name: "Raptor", species: "Velociraptor" };
157enclosure.addDinosaur(dino);
158console.log("Dinosaure dans l'enclos :", enclosure.dinosaurs.size);
159
160const encRef = dino.enclosureRef.deref();
161console.log("Enclos via WeakRef :", encRef ? encRef.name : "GC collected");
162
163console.log("\nTous les memory leaks corrigés !");
164console.log("Bonnes pratiques :");
165console.log("1. Retire toujours les event listeners (destroy/cleanup)");
166console.log("2. Limite la taille des tableaux (ring buffer / maxSize)");
167console.log("3. Nettoie les timers (clearInterval/clearTimeout)");
168console.log("4. Utilise WeakRef/WeakMap pour les références circulaires");Tu as repéré une erreur dans cette leçon ?
Tâches pratiques dans le jeu
- Éditeur de code
Implémente DeepPartial et NonNullable.