"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."
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.
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};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};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};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));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}Next.js ma wbudowane wsparcie dla TypeScript - po prostu utwórz plik
.ts lub .tsx i Next.js automatycznie skonfiguruje TypeScript!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;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}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}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};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};"Widzisz?" uśmiecha się Dr. Sattler. "TypeScript świetnie integruje się z wszystkimi popularnymi frameworkami:"
React.FC<Props>useState<T>, useRef<HTMLElement>)getServerSideProps, getStaticPropsKluczowe korzyści:
"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!"