Kurs JavaScript i React · Moduł 12: React i TypeScript

Typowanie Zdarzeń - ChangeEvent, FormEvent, MouseEvent

5 min czytania
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. 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

Przydatne artykuły