Usamos cookies para mejorar tu experiencia en el sitio
CodeWorlds

TypeScript z popularnymi frameworkami

"Witaj z powrotem w Parku Jurajskim," mówi Dr. Ellie Sattler, paleo-botanik. "Poznałeś już TypeScript jako samodzielny język. Ale w prawdziwym świecie rzadko używamy czystego TypeScript - zazwyczaj integrujemy go z frameworkami, które pomagają budować złożone aplikacje. Chodź, pokażę Ci jak TypeScript współpracuje z najpopularniejszymi narzędziami."

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

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};

Typowanie React Hooks

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};

Custom Hooks z TypeScript

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};

TypeScript z Node.js/Express - Backend API

Typowanie Express aplikacji

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));

Typowane serwisy i repozytoria

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}

TypeScript z Next.js - Full-stack z natywnym wsparciem

Next.js ma wbudowane wsparcie dla TypeScript - po prostu utwórz plik

.ts
lub
.tsx
i Next.js automatycznie skonfiguruje TypeScript!

Pages i API Routes

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;

API Routes w Next.js

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}

App Router (Next.js 13+) z Server Components

1// app/dinosaurs/[id]/page.tsx
2interface PageProps {
3  params: { id: string };
4  searchParams: { [key: string]: string | string[] | undefined };
5}
6
7// Server Component
8export default async function DinosaurDetailPage({ params }: PageProps) {
9  const dinosaur = await getDinosaurById(params.id);
10
11  return (
12    <div>
13      <h1>{dinosaur.name}</h1>
14      <DinosaurDetails dino={dinosaur} />
15    </div>
16  );
17}
18
19// Client Component
20'use client';
21
22interface DinosaurDetailsProps {
23  dino: Dinosaur;
24}
25
26export function DinosaurDetails({ dino }: DinosaurDetailsProps) {
27  const [isFavorite, setIsFavorite] = useState(false);
28
29  return (
30    <div>
31      <button onClick={() => setIsFavorite(!isFavorite)}>
32        {isFavorite ? '⭐' : '☆'} Ulubiony
33      </button>
34      {/* Reszta komponentu */}
35    </div>
36  );
37}

Wzorce typowania w frameworkach

Generic Components

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};

Form handling z TypeScript

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        })}
54        placeholder="Numer zagrody"
55      />
56
57      <label>
58        <input type="checkbox" {...register('isDangerous')} />
59        Niebezpieczny gatunek
60      </label>
61
62      <button type="submit">Dodaj dinozaura</button>
63    </form>
64  );
65};

Podsumowanie

"Widzisz?" uśmiecha się Dr. Sattler. "TypeScript świetnie integruje się z wszystkimi popularnymi frameworkami:"

React

  • Silne typowanie komponentów przez
    React.FC<Props>
  • Typowane hooks (
    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) dla walidacji

Next.js

  • Natywne wsparcie TypeScript - zero konfiguracji
  • Typowane
    getServerSideProps
    ,
    getStaticProps
  • API Routes z typowaniem
  • Server i Client Components w App Router

Kluczowe korzyści:

  • Łapanie błędów już podczas pisania kodu
  • Autocomplete i IntelliSense
  • Refactoring bez obaw
  • Samodokumentujący się kod
  • Łatwiejsza współpraca w zespole

"TypeScript," kończy Dr. Sattler, "to jak dodanie zabezpieczeń do naszych systemów w Parku Jurajskim. Na początku wymaga trochę więcej pracy, ale w dłuższej perspektywie oszczędza nam mnóstwo problemów!"

Ir a CodeWorlds