Kurs JavaScript i React · Moduł 12: React i TypeScript
Typowanie Zdarzeń - ChangeEvent, FormEvent, MouseEvent
W tej lekcji6
Wydzielasz funkcję obsługi kliknięcia poza JSX, piszesz (event) => ... i nagle edytor nie wie nic o event: ani że ma pole value, ani że da się wywołać preventDefault(). Zdarzenia w React to sygnały z różnych części statku kosmicznego - kliknięcia przycisków, wprowadzanie danych, przesyłanie formularzy. TypeScript pozwala precyzyjnie określić, jaki sygnał otrzymujesz i z jakiego źródła.
Dobra wiadomość na start: gdy piszesz funkcję bezpośrednio w atrybucie, na przykład onChange={e => ...}, TypeScript sam wydedukuje typ zdarzenia. Jawny typ jest potrzebny dopiero wtedy, gdy handler to osobna funkcja - a tak robi się w większości prawdziwych komponentów.
Podstawowe typy zdarzeń
React dostarcza własne typy zdarzeń, które są generyczne - w nawiasach ostrych podajesz, z jakiego elementu HTML pochodzi zdarzenie. Wszystkie wywodzą się z bazowego typu React.SyntheticEvent, czyli opakowania, którym React otacza natywne zdarzenie przeglądarki. Najczęstsze to MouseEvent (mysz), ChangeEvent (zmiana wartości pola) i SubmitEvent (wysłanie formularza):
1import React from 'react';
2
3// Zdarzenie kliknięcia
4const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
5 console.log("Kliknięto przycisk:", event.currentTarget.textContent);
6};
7
8// Zdarzenie zmiany wartości
9const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
10 console.log("Nowa wartość:", event.target.value);
11};
12
13// Zdarzenie przesłania formularza
14const handleSubmit = (event: React.SubmitEvent<HTMLFormElement>) => {
15 event.preventDefault();
16 console.log("Formularz przesłany!");
17};Parametr w nawiasach ostrych to element, do którego podpięto handler. Dzięki niemu event.currentTarget w pierwszym handlerze ma typ HTMLButtonElement, a event.target.value w drugim jest tekstem. Metoda preventDefault() zatrzymuje domyślne przeładowanie strony po wysłaniu formularza.
FormEvent - starsza nazwa
Przez lata formularze typowano jako React.FormEvent i tak wygląda ogromna część istniejącego kodu, a także tytuł tej lekcji. Od @types/react 19.2.10 (styczeń 2026) FormEvent jest oznaczony jako przestarzały (deprecated), bo nie odpowiada żadnemu prawdziwemu zdarzeniu przeglądarki. Zamiast niego używaj SubmitEvent dla onSubmit, ChangeEvent dla onChange albo ogólnego SyntheticEvent.
1// Starszy zapis - nadal się kompiluje, ale jest deprecated
2const handleSubmitLegacy = (event: React.FormEvent<HTMLFormElement>) => {
3 event.preventDefault();
4};Ten kod wciąż działa i przejdzie kompilację, edytor jedynie przekreśli nazwę typu. Jeśli Twój projekt ma starsze typy niż 19.2.10, SubmitEvent nie będzie jeszcze dostępny - wtedy zostań przy FormEvent i zaktualizuj pakiet przy najbliższej okazji.
ChangeEvent - różne elementy formularzy
Typ ChangeEvent różni się w zależności od elementu, na którym nasłuchujesz. Pole tekstowe, pole wieloliniowe i lista rozwijana mają value typu string, a checkbox dodatkowo checked typu boolean:
1// Input tekstowy
2const handleTextChange = (e: React.ChangeEvent<HTMLInputElement>) => {
3 const value: string = e.target.value;
4 const name: string = e.target.name;
5};
6
7// Textarea
8const handleTextareaChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
9 const value: string = e.target.value;
10};
11
12// Select
13const handleSelectChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
14 const selectedValue: string = e.target.value;
15};
16
17// Checkbox
18const handleCheckboxChange = (e: React.ChangeEvent<HTMLInputElement>) => {
19 const isChecked: boolean = e.target.checked;
20};Zwróć uwagę, że checkbox to też HTMLInputElement - zmienia się tylko pole, które odczytujesz. Uwaga: value jest zawsze tekstem, nawet w <input type="number">. Liczbę uzyskasz przez Number(e.target.value) albo e.target.valueAsNumber.
MouseEvent - kliknięcia i ruch myszy
MouseEvent niesie współrzędne kursora (clientX, clientY) i informację o elemencie. Ten sam typ obsługuje kliknięcie oraz najechanie myszą:
1// Kliknięcie przycisku
2const handleButtonClick = (e: React.MouseEvent<HTMLButtonElement>) => {
3 console.log("Pozycja kliknięcia:", e.clientX, e.clientY);
4};
5
6// Kliknięcie na div
7const handleDivClick = (e: React.MouseEvent<HTMLDivElement>) => {
8 const target = e.currentTarget; // HTMLDivElement
9};
10
11// Najechanie myszą
12const handleMouseEnter = (e: React.MouseEvent<HTMLDivElement>) => {
13 console.log("Mysz najechała na element");
14};currentTarget to zawsze element z handlerem, dlatego ma precyzyjny typ. target przy kliknięciu może być dowolnym dzieckiem wewnątrz, więc TypeScript typuje go ogólnie.
KeyboardEvent - zdarzenia klawiatury
Dla klawiatury używasz KeyboardEvent, a naciśnięty klawisz odczytasz z pola key:
1const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
2 if (e.key === "Enter") {
3 console.log("Enter naciśnięty!");
4 }
5 if (e.key === "Escape") {
6 console.log("Escape naciśnięty!");
7 }
8};Pole key zawiera czytelną nazwę klawisza, na przykład "Enter" lub "a". Starsze pole keyCode jest przestarzałe, więc nie używaj go w nowym kodzie.
Praktyczny przykład - formularz z typowaniem
Złóżmy wszystko w formularz planowania misji. Najpierw typ danych formularza, potem stan otypowany generykiem, jak w poprzedniej lekcji:
1interface MissionFormData {
2 name: string;
3 destination: string;
4 crewSize: number;
5 priority: "low" | "medium" | "high";
6}
7
8function MissionForm() {
9 const [formData, setFormData] = useState<MissionFormData>({
10 name: "",
11 destination: "",
12 crewSize: 1,
13 priority: "medium"
14 });Teraz trzy handlery. Zapis [name]: value ustawia pole o nazwie pobranej z atrybutu name pola. Słowo as to asercja typu: mówimy kompilatorowi, że tekst z listy na pewno jest jednym z trzech priorytetów, bo sami zdefiniowaliśmy opcje. Używaj as oszczędnie - wyłącza sprawdzanie w tym miejscu.
1 const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
2 const { name, value } = e.target;
3 setFormData(prev => ({ ...prev, [name]: value }));
4 };
5
6 const handleSelectChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
7 setFormData(prev => ({
8 ...prev,
9 priority: e.target.value as MissionFormData["priority"]
10 }));
11 };
12
13 const handleSubmit = (e: React.SubmitEvent<HTMLFormElement>) => {
14 e.preventDefault();
15 console.log("Misja zaplanowana:", formData);
16 };Na koniec JSX, w którym podpinamy handlery. Typ elementu w każdym handlerze musi zgadzać się z elementem w JSX - handler dla select nie zostanie przyjęty przez input.
1 return (
2 <form onSubmit={handleSubmit}>
3 <input name="name" value={formData.name} onChange={handleInputChange} />
4 <input name="destination" value={formData.destination} onChange={handleInputChange} />
5 <select value={formData.priority} onChange={handleSelectChange}>
6 <option value="low">Niski</option>
7 <option value="medium">Średni</option>
8 <option value="high">Wysoki</option>
9 </select>
10 <button type="submit">Zaplanuj misję</button>
11 </form>
12 );
13}Formularz działa jak zwykły formularz React - typy nie dodały ani jednego atrybutu w JSX, za to każdy handler jest teraz sprawdzany przez kompilator.
Funkcje jako props - typowanie callbacków
Gdy komponent przyjmuje handler od rodzica, typ funkcji zapisujesz w interfejsie props. void oznacza, że wynik funkcji nas nie interesuje:
1interface ActionButtonProps {
2 label: string;
3 onClick: (e: React.MouseEvent<HTMLButtonElement>) => void;
4 onHover?: (e: React.MouseEvent<HTMLButtonElement>) => void;
5}
6
7function ActionButton({ label, onClick, onHover }: ActionButtonProps) {
8 return (
9 <button onClick={onClick} onMouseEnter={onHover}>
10 {label}
11 </button>
12 );
13}Moja rada: nie zgaduj typu zdarzenia - najedź kursorem na atrybut onChange w edytorze, a TypeScript pokaże dokładny typ do skopiowania. W lekcji o komponentach generycznych wykorzystasz te handlery w uniwersalnym polu wyboru.
Kod do tej lekcji: App.tsx
1import React, { useState } from 'react';
2
3// Interfejs danych formularza misji
4interface MissionFormData {
5 name: string;
6 destination: string;
7 crewSize: number;
8 priority: "low" | "medium" | "high";
9 notes: string;
10}
11
12// Interfejs misji (z id i datą)
13interface Mission extends MissionFormData {
14 id: number;
15 createdAt: string;
16}
17
18function App() {
19 const [missions, setMissions] = useState<Mission[]>([]);
20 const [formData, setFormData] = useState<MissionFormData>({
21 name: "",
22 destination: "",
23 crewSize: 1,
24 priority: "medium",
25 notes: ""
26 });
27
28 // Typowane zdarzenia formularza
29 const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
30 const { name, value, type } = e.target;
31 setFormData(prev => ({
32 ...prev,
33 [name]: type === "number" ? Number(value) : value
34 }));
35 };
36
37 const handleTextareaChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
38 setFormData(prev => ({ ...prev, notes: e.target.value }));
39 };
40
41 const handleSelectChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
42 setFormData(prev => ({
43 ...prev,
44 priority: e.target.value as MissionFormData["priority"]
45 }));
46 };
47
48 const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
49 e.preventDefault();
50 const newMission: Mission = {
51 ...formData,
52 id: Date.now(),
53 createdAt: new Date().toLocaleString()
54 };
55 setMissions(prev => [...prev, newMission]);
56 setFormData({ name: "", destination: "", crewSize: 1, priority: "medium", notes: "" });
57 };
58
59 const handleDelete = (e: React.MouseEvent<HTMLButtonElement>, id: number) => {
60 e.stopPropagation();
61 setMissions(prev => prev.filter(m => m.id !== id));
62 };
63
64 const priorityColors = { low: "#22c55e", medium: "#f59e0b", high: "#ef4444" };
65
66 return (
67 <div className="app">
68 <h1>Planowanie Misji Kosmicznych</h1>
69
70 <form onSubmit={handleSubmit} className="form">
71 <input name="name" value={formData.name} onChange={handleInputChange}
72 placeholder="Nazwa misji" required />
73 <input name="destination" value={formData.destination} onChange={handleInputChange}
74 placeholder="Cel misji" required />
75 <input name="crewSize" type="number" min={1} max={20}
76 value={formData.crewSize} onChange={handleInputChange} />
77 <select value={formData.priority} onChange={handleSelectChange}>
78 <option value="low">Niski priorytet</option>
79 <option value="medium">Sredni priorytet</option>
80 <option value="high">Wysoki priorytet</option>
81 </select>
82 <textarea value={formData.notes} onChange={handleTextareaChange}
83 placeholder="Notatki..." rows={2} />
84 <button type="submit">Zaplanuj misje</button>
85 </form>
86
87 <div className="missions">
88 {missions.map(mission => (
89 <div key={mission.id} className="mission-card">
90 <h3>{mission.name}</h3>
91 <p>Cel: {mission.destination}</p>
92 <p>Zaloga: {mission.crewSize} os.</p>
93 <span className="priority" style={{ color: priorityColors[mission.priority] }}>
94 {mission.priority.toUpperCase()}
95 </span>
96 {mission.notes && <p className="notes">{mission.notes}</p>}
97 <p className="date">{mission.createdAt}</p>
98 <button className="delete" onClick={(e) => handleDelete(e, mission.id)}>Usun</button>
99 </div>
100 ))}
101 {missions.length === 0 && <p className="empty">Brak zaplanowanych misji</p>}
102 </div>
103 </div>
104 );
105}
106
107export default App;Pamiętaj: typ zdarzenia mówi, z którego pulpitu statku przyszedł sygnał - znając źródło, wiesz, jakie dane niesie.
Widzisz błąd w tej lekcji?
Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. Jaki typ zdarzenia należy użyć w handlerze onChange dla elementu <input>?
Zadania praktyczne w grze
- Układanie w pionie
Ułóż typy zdarzeń od najbardziej ogólnego do najbardziej specyficznego:
- Edytor kodu
Typowanie zdarzeń onChange i onSubmit
- Klikanie w kolejności
Ułóż sygnaturę typowanego handlera kliknięcia przycisku:
- Edytor kodu
Typowanie zdarzeń myszy i klawiatury