Kurs JavaScript i TypeScript · Moduł 10: TypeScript w praktyce

TypeScript z popularnymi frameworkami

13 min czytania
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 getServerSideProps i getStaticProps
  • 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.

Przydatne artykuły