Kurs JavaScript i TypeScript · Moduł 10: TypeScript w praktyce
TypeScript z popularnymi frameworkami
W tej lekcji5
"Witaj z powrotem w Parku Jurajskim" - mówi Dr. Rex. "Poznałeś już TypeScript jako samodzielny język, ale w prawdziwym świecie rzadko używamy go w czystej postaci. Zwykle łączymy go z frameworkami, które pomagają budować złożone aplikacje." Bez typów literówka w nazwie propa wyjdzie dopiero wtedy, gdy karta Velociraptora pokaże napis Bezpieczny.
TypeScript z React - Komponenty z typami
React to jedna z najpopularniejszych bibliotek do budowania interfejsów użytkownika. TypeScript świetnie się z nią integruje, zapewniając silne typowanie komponentów, props i hooków.
Podstawowe komponenty funkcyjne
Props opisujesz interfejsem, w którym znak ? oznacza pole opcjonalne, a komponent dostaje typ React.FC<DinoCardProps>:
1// Interfejs dla props komponentu
2interface DinoCardProps {
3 name: string;
4 species: string;
5 isDangerous: boolean;
6 age?: number; // Opcjonalne pole
7 onEscape?: () => void; // Opcjonalna funkcja callback
8}
9
10// Komponent funkcyjny z typowanymi props
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>Gatunek: {species}</p>
22 <p>Wiek: {age || 'Nieznany'}</p>
23 <div className={isDangerous ? 'danger' : 'safe'}>
24 {isDangerous ? 'NIEBEZPIECZNY' : 'Bezpieczny'}
25 </div>
26 {onEscape && (
27 <button onClick={onEscape}>
28 Raport o ucieczce
29 </button>
30 )}
31 </div>
32 );
33};
34
35// Użycie z pełnym typowaniem
36const App = () => {
37 const handleEscape = () => {
38 console.log('ALARM: Dinozaur uciekł!');
39 };
40
41 return (
42 <DinoCard
43 name="Blue"
44 species="Velociraptor"
45 isDangerous={true}
46 age={4}
47 onEscape={handleEscape}
48 />
49 );
50};Brak propa species to błąd kompilacji już w App. Wiele zespołów zamiast React.FC pisze po prostu function DinoCard(props: DinoCardProps), bo od React 18 oba zapisy działają praktycznie tak samo. Przy liczbach lepsze jest ?? niż ||, bo wiek 0 też jest poprawny.
Typowanie React Hooks
Hooki są generyczne. Przy prostych wartościach TypeScript sam wywnioskuje typ, ale dla pustej tablicy albo null podajesz go w nawiasach ostrych:
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 z typem
12 const [dinosaurs, setDinosaurs] = useState<Dinosaur[]>([]);
13 const [loading, setLoading] = useState<boolean>(false);
14 const [error, setError] = useState<Error | null>(null);
15
16 // useRef z typem elementu DOM
17 const alertSound = useRef<HTMLAudioElement>(null);
18
19 // useEffect - fetchowanie danych
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 z typowanymi parametrami
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 // Odtwórz alarm
48 if (alertSound.current) {
49 alertSound.current.play();
50 }
51 }, []);
52
53 // useMemo z typem zwracanym
54 const dangerousDinos = useMemo<Dinosaur[]>(() => {
55 return dinosaurs.filter(dino => dino.status === 'escaped');
56 }, [dinosaurs]);
57
58 if (loading) return <div>Ładowanie danych monitoringu...</div>;
59 if (error) return <div>Błąd: {error.message}</div>;
60
61 return (
62 <div>
63 <h1>Monitor Dinozaurów</h1>
64 <audio ref={alertSound} src="/sounds/alarm.mp3" />
65
66 {dangerousDinos.length > 0 && (
67 <div className="alert">
68 UWAGA: {dangerousDinos.length} dinozaurów na wolności!
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};Bez <Dinosaur[]> pusta tablica miałaby typ never[] i nie dałoby się do niej nic dodać. useRef<HTMLAudioElement>(null) wie, że current to element audio albo null, więc kod sprawdza go przed play(), a as const zachowuje literał 'escaped' zamiast poszerzać go do string.
Custom Hooks z TypeScript
Własny hook zwraca obiekt opisany interfejsem, więc komponent dostaje podpowiedzi do każdej funkcji. Omit<Dinosaur, 'id'> oznacza dinozaura bez id, które nada serwer:
1// Custom hook do zarządzania listą dinozaurów
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// Użycie custom hooka
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 {/* Komponent wykorzystujący hook */}
81 </div>
82 );
83};Dinosaur['status'] pilnuje, żeby updateStatus przyjmował wyłącznie jeden z trzech statusów.
TypeScript z Node.js/Express - Backend API
Typowanie Express aplikacji
Po stronie serwera typujesz żądania, odpowiedzi i middleware, a kontroler grupuje handlery jednego zasobu:
1import express, { Request, Response, NextFunction } from 'express';
2
3// Interfejsy dla modeli danych
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// Rozszerzenie Request o custom pola
19interface AuthRequest extends Request {
20 user?: {
21 id: number;
22 role: 'admin' | 'keeper' | 'visitor';
23 };
24}
25
26// Controller z typowaniem
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 z typowaniem
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) jest konieczne, bo metoda przekazana jako callback traci this. Pole this.dinosaurService trzeba jeszcze zadeklarować i wstrzyknąć przez konstruktor, a zamiast interfejsu AuthRequest możesz użyć augmentacji express-serve-static-core z poprzedniej lekcji.
Typowane serwisy i repozytoria
Wzorzec Repository ukrywa bazę danych za generycznym interfejsem Repository<T>, a serwis zawiera logikę biznesową, taką jak walidacja:
1// Repository Pattern z 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 // Implementacja z bazą danych
13 return [];
14 }
15
16 async findById(id: number): Promise<Dinosaur | null> {
17 // Implementacja
18 return null;
19 }
20
21 async create(data: Omit<Dinosaur, 'id'>): Promise<Dinosaur> {
22 // Implementacja
23 return { id: 1, ...data } as Dinosaur;
24 }
25
26 async update(id: number, data: Partial<Dinosaur>): Promise<Dinosaur> {
27 // Implementacja
28 return {} as Dinosaur;
29 }
30
31 async delete(id: number): Promise<void> {
32 // Implementacja
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 // Walidacja
54 this.validateDinosaurData(data);
55
56 // Tworzenie
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 // Logika generowania harmonogramu
74 return ['08:00', '14:00', '20:00'];
75 }
76}Serwis zależy od konkretnej klasy DinosaurRepository, a zgodnie z zasadą odwrócenia zależności lepiej przyjąć Repository<Dinosaur> i w testach podać repozytorium w pamięci. Partial<T> działa tylko na pierwszym poziomie, więc dla zagnieżdżonych obiektów pisze się rekurencyjny DeepPartial<T>, a wbudowany NonNullable<T> usuwa z typu null i undefined.
TypeScript z Next.js - Full-stack z natywnym wsparciem
Next.js ma wbudowane wsparcie dla TypeScript: wystarczy utworzyć plik .ts lub .tsx, a Next.js sam skonfiguruje kompilator i utworzy tsconfig.json.
Pages i API Routes
W Pages Router strona dostaje typ NextPage<Props>, a funkcja pobierająca dane typ GetServerSideProps<Props>, więc props strony i dane z serwera muszą do siebie pasować:
1// pages/dinosaurs/[id].tsx - Dynamic route z typowaniem
2import { GetServerSideProps, NextPage } from 'next';
3
4interface DinosaurPageProps {
5 dinosaur: Dinosaur;
6 lastUpdated: string;
7}
8
9// Komponent strony
10const DinosaurPage: NextPage<DinosaurPageProps> = ({ dinosaur, lastUpdated }) => {
11 return (
12 <div>
13 <h1>{dinosaur.name}</h1>
14 <p>Gatunek: {dinosaur.species}</p>
15 <p>Zagroda: {dinosaur.paddock}</p>
16 <small>Ostatnia aktualizacja: {lastUpdated}</small>
17 </div>
18 );
19};
20
21// Server-side rendering z typowaniem
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;Jeśli getServerSideProps zwróci obiekt bez lastUpdated, kompilator zgłosi błąd, zanim strona trafi do zwiedzających.
API Routes w Next.js
Trasa API w Pages Router to funkcja z typami NextApiRequest i NextApiResponse<T>, gdzie T opisuje kształt odpowiedzi:
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 ma typ string | string[] | undefined, dlatego kod zamienia go na liczbę, a nieobsługiwana metoda dostaje status 405.
App Router (Next.js 13+) z Server Components
W App Router strona jest domyślnie Server Component, który może być funkcją async i pobierać dane bezpośrednio. Interaktywne fragmenty wydzielasz do Client Components:
1// app/dinosaurs/[id]/page.tsx
2// Od Next.js 15 params i searchParams to obiekty 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 - osobny plik, np. app/dinosaurs/[id]/DinosaurDetails.tsx,
22// bo dyrektywa 'use client' musi stać na samym początku pliku
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 ? '★' : '☆'} Ulubiony
36 </button>
37 {/* Reszta komponentu */}
38 </div>
39 );
40}Od Next.js 15 params jest obiektem Promise, więc najpierw go oczekujesz, a w wersjach 13 i 14 był zwykłym obiektem. Dyrektywa 'use client' musi stać na początku osobnego pliku, dlatego DinosaurDetails mieszka we własnym module.
Wzorce typowania w frameworkach
Generic Components
Komponent generyczny działa dla dowolnego typu elementów, a TypeScript pilnuje, żeby renderItem i keyExtractor pasowały do przekazanej tablicy:
1// Uniwersalny komponent listy z typowaniem
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 || 'Brak elementów'}</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// Użycie
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="Brak dinozaurów w systemie"
42 />
43 );
44};Zapis List<Dinosaur> w JSX jawnie podaje typ, choć TypeScript wywnioskowałby go z items, a ten sam komponent wyświetli też listę wybiegów.
Form handling z TypeScript
Biblioteka react-hook-form przyjmuje typ danych formularza, więc register akceptuje tylko istniejące nazwy pól:
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('Dinozaur dodany!');
28 }
29 } catch (error) {
30 console.error('Błąd podczas dodawania:', error);
31 }
32 };
33
34 return (
35 <form onSubmit={handleSubmit(onSubmit)}>
36 <input
37 {...register('name', { required: 'Nazwa jest wymagana' })}
38 placeholder="Nazwa dinozaura"
39 />
40 {errors.name && <span>{errors.name.message}</span>}
41
42 <input
43 {...register('species', { required: true })}
44 placeholder="Gatunek"
45 />
46
47 <input
48 type="number"
49 {...register('paddock', {
50 required: true,
51 min: 1,
52 max: 10,
53 valueAsNumber: true // bez tego wartość z pola number jest napisem
54 })}
55 placeholder="Numer zagrody"
56 />
57
58 <label>
59 <input type="checkbox" {...register('isDangerous')} />
60 Niebezpieczny gatunek
61 </label>
62
63 <button type="submit">Dodaj dinozaura</button>
64 </form>
65 );
66};Literówka w nazwie pola to błąd kompilacji. Pole type="number" zwraca w przeglądarce napis, dlatego opcja valueAsNumber zamienia go na liczbę zgodną z typem paddock: number.
Podsumowanie
"Widzisz?" uśmiecha się Dr. Rex. "TypeScript świetnie integruje się z popularnymi frameworkami."
React
- Silne typowanie komponentów przez
React.FC<Props> - Typowane hooki (
useState<T>,useRef<HTMLElement>) - Custom hooks z pełnym type safety
- Generic components dla reużywalności
Node.js/Express
- Typowanie Request, Response, NextFunction
- Repository i Service patterns z interfejsami
- Middleware z custom Request types
- DTO (Data Transfer Objects) opisujące dane wejściowe
Next.js
- Natywne wsparcie TypeScript bez ręcznej konfiguracji
- Typowane
getServerSidePropsigetStaticProps - API Routes z typowaniem
- Server i Client Components w App Router
Kluczowe korzyści to łapanie błędów już podczas pisania kodu, autouzupełnianie (IntelliSense), refaktoryzacja bez obaw, samodokumentujący się kod i łatwiejsza współpraca w zespole. Nowy projekt zaczynaj od razu w TypeScript z trybem strict bo dopisywanie typów do gotowej aplikacji kosztuje więcej. W kolejnej lekcji połączysz wiedzę z całego modułu w projekcie końcowym: systemie diagnostycznym parku.
"TypeScript to zabezpieczenie systemów parku, które na początku wymaga trochę pracy, ale w dłuższej perspektywie oszczędza mnóstwo problemów" - kończy Dr. Rex.
Kod do tej lekcji: index.js
1// Profiling pamięci - znajdź i napraw memory leaks
2console.log("Park Jurajski - Laboratorium Profilingu Pamięci");
3console.log("Znajdź i napraw memory leaks w kodzie!\n");
4
5// ====================================
6// ZADANIE: Poniższy kod ma kilka memory leaks.
7// Napraw je stosując dobre praktyki.
8// ====================================
9
10// LEAK 1: Event listeners bez cleanup
11// Napraw: Dodaj metodę destroy() z removeEventListener
12class DinosaurTracker {
13 constructor(name) {
14 this.name = name;
15 this.positions = [];
16 this.handler = this.onPositionUpdate.bind(this);
17 // Symulacja addEventListener
18 console.log(`Tracker dla ${name} - listener dodany`);
19 }
20
21 onPositionUpdate(position) {
22 this.positions.push(position);
23 }
24
25 // TODO: Dodaj metodę destroy() która usunie event listener
26 destroy() {
27 // Rozwiązanie: usunięcie listenera
28 // removeEventListener('position', this.handler);
29 this.positions = [];
30 this.handler = null;
31 console.log(`Tracker dla ${this.name} - listener usunięty, pamięć zwolniona`);
32 }
33}
34
35// LEAK 2: Rosnąca tablica bez limitu
36// Napraw: Ogranicz rozmiar tablicy
37class AlertSystem {
38 constructor() {
39 this.alerts = [];
40 this.maxAlerts = 100; // TODO: Ustaw limit
41 }
42
43 addAlert(message) {
44 // TODO: Sprawdź limit i usuń najstarsze alerty
45 if (this.alerts.length >= this.maxAlerts) {
46 this.alerts.shift(); // Usuń najstarszy
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: Zapomniane timery
60// Napraw: Dodaj cleanup dla timerów
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(`Monitoring ${dinoName} started`);
78 }
79
80 // TODO: Dodaj metodę stopAll() czyszczącą timery
81 stopAll() {
82 this.timers.forEach(timer => clearInterval(timer));
83 this.timers = [];
84 this.data = [];
85 console.log("Wszystkie monitory zatrzymane, timery wyczyszczone");
86 }
87}
88
89// LEAK 4: Circular references
90// Napraw: Użyj WeakRef lub WeakMap
91class Enclosure {
92 constructor(name) {
93 this.name = name;
94 this.dinosaurs = [];
95 }
96
97 addDinosaur(dino) {
98 this.dinosaurs.push(dino);
99 // Circular reference: dino -> enclosure -> dino
100 dino.enclosure = this;
101 }
102}
103
104// Rozwiązanie z 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 nie blokuje 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// TESTY
125// ====================================
126console.log("=== Test naprawionych leaków ===\n");
127
128// Test 1: Tracker z cleanup
129const tracker = new DinosaurTracker("T-Rex");
130tracker.onPositionUpdate({ x: 10, y: 20 });
131tracker.onPositionUpdate({ x: 15, y: 25 });
132console.log("Pozycje przed destroy:", tracker.positions.length);
133tracker.destroy();
134console.log("Pozycje po destroy:", tracker.positions.length);
135
136// Test 2: Alert system z limitem
137console.log("\n--- Alert System ---");
138const alertSys = new AlertSystem();
139for (let i = 0; i < 150; i++) {
140 alertSys.addAlert(`Alert #${i}`);
141}
142console.log(`Alerty po 150 dodaniach (limit 100): ${alertSys.getAlertCount()}`);
143
144// Test 3: Health monitor z cleanup
145console.log("\n--- Health Monitor ---");
146const healthMon = new HealthMonitor();
147healthMon.startMonitoring("Velociraptor", 1000);
148healthMon.startMonitoring("Stegosaurus", 2000);
149console.log("Aktywne timery:", healthMon.timers.length);
150healthMon.stopAll();
151console.log("Timery po stopAll:", healthMon.timers.length);
152
153// Test 4: WeakRef rozwiązanie
154console.log("\n--- Enclosure z WeakRef ---");
155const enclosure = new EnclosureFixed("Paddock A");
156let dino = { name: "Raptor", species: "Velociraptor" };
157enclosure.addDinosaur(dino);
158console.log("Dinozaur w zagrodzie:", enclosure.dinosaurs.size);
159
160const encRef = dino.enclosureRef.deref();
161console.log("Enclosure przez WeakRef:", encRef ? encRef.name : "GC collected");
162
163console.log("\nWszystkie memory leaks naprawione!");
164console.log("Dobre praktyki:");
165console.log("1. Zawsze usuwaj event listeners (destroy/cleanup)");
166console.log("2. Ogranicz rozmiar tablic (ring buffer / maxSize)");
167console.log("3. Czyść timery (clearInterval/clearTimeout)");
168console.log("4. Użyj WeakRef/WeakMap dla circular references");Widzisz błąd w tej lekcji?
Zadania praktyczne w grze
- Edytor kodu
Zaimplementuj DeepPartial i NonNullable.