JavaScript and React course Β· Module 12: React and TypeScript
Typing Events - ChangeEvent, FormEvent, MouseEvent
In this lesson6
You move a click handler out of the JSX, write (event) => ..., and suddenly the editor knows nothing about event: neither that it has a value field nor that you can call preventDefault(). Events in React are signals from different parts of the spaceship - button clicks, data input, form submissions. TypeScript lets you precisely define which signal you receive and from which source.
Good news to start with: when you write a function directly in the attribute, for example onChange={e => ...}, TypeScript infers the event type by itself. You only need an explicit type when the handler is a separate function - and that's how most real components are written.
Basic Event Types
React provides its own event types, which are generic - in angle brackets you specify which HTML element the event comes from. They all derive from the base type React.SyntheticEvent, the wrapper React puts around the browser's native event. The most common are MouseEvent (mouse), ChangeEvent (field value change) and SubmitEvent (form submission):
1import React from 'react';
2
3// Click event
4const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
5 console.log("Button clicked:", event.currentTarget.textContent);
6};
7
8// Value change event
9const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
10 console.log("New value:", event.target.value);
11};
12
13// Form submission event
14const handleSubmit = (event: React.SubmitEvent<HTMLFormElement>) => {
15 event.preventDefault();
16 console.log("Form submitted!");
17};The parameter in angle brackets is the element the handler is attached to. Thanks to it, event.currentTarget in the first handler has the type HTMLButtonElement, and event.target.value in the second is a string. The preventDefault() method stops the default page reload after a form submission.
FormEvent - the Older Name
For years forms were typed with React.FormEvent, and that's what a huge amount of existing code looks like, as does the title of this lesson. Since @types/react 19.2.10 (January 2026) FormEvent is marked as deprecated, because it doesn't match any real browser event. Use SubmitEvent for onSubmit, ChangeEvent for onChange, or the general SyntheticEvent instead.
1// Older notation - still compiles, but is deprecated
2const handleSubmitLegacy = (event: React.FormEvent<HTMLFormElement>) => {
3 event.preventDefault();
4};This code still works and compiles, the editor just strikes through the type name. If your project has types older than 19.2.10, SubmitEvent won't be available yet - stay with FormEvent and update the package when you can.
ChangeEvent - Different Form Elements
The ChangeEvent type differs depending on the element you're listening on. A text input, a textarea and a select all have a value of type string, and a checkbox additionally has checked of type boolean:
1// Text input
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};Note that a checkbox is also an HTMLInputElement - only the field you read changes. Watch out: value is always a string, even in <input type="number">. You get a number with Number(e.target.value) or e.target.valueAsNumber.
MouseEvent - Clicks and Mouse Movement
MouseEvent carries the cursor coordinates (clientX, clientY) and information about the element. The same type handles both a click and a hover:
1// Button click
2const handleButtonClick = (e: React.MouseEvent<HTMLButtonElement>) => {
3 console.log("Click position:", e.clientX, e.clientY);
4};
5
6// Click on a div
7const handleDivClick = (e: React.MouseEvent<HTMLDivElement>) => {
8 const target = e.currentTarget; // HTMLDivElement
9};
10
11// Mouse hover
12const handleMouseEnter = (e: React.MouseEvent<HTMLDivElement>) => {
13 console.log("Mouse entered the element");
14};currentTarget is always the element with the handler, so it has a precise type. target on a click can be any child inside, so TypeScript types it generally.
KeyboardEvent - Keyboard Events
For the keyboard you use KeyboardEvent, and you read the pressed key from the key field:
1const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
2 if (e.key === "Enter") {
3 console.log("Enter pressed!");
4 }
5 if (e.key === "Escape") {
6 console.log("Escape pressed!");
7 }
8};The key field holds a readable key name, such as "Enter" or "a". The older keyCode field is deprecated, so don't use it in new code.
Practical Example - Form with Typing
Let's put everything together in a mission planning form. First the form data type, then state typed with a generic, as in the previous lesson:
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 });Now three handlers. The [name]: value notation sets the field whose name comes from the input's name attribute. The as keyword is a type assertion: we tell the compiler that the text from the list is definitely one of the three priorities, because we defined the options ourselves. Use as sparingly - it switches off checking at that spot.
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("Mission planned:", formData);
16 };Finally the JSX where we attach the handlers. The element type in each handler must match the element in JSX - a handler for select won't be accepted by an 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">Low</option>
7 <option value="medium">Medium</option>
8 <option value="high">High</option>
9 </select>
10 <button type="submit">Plan mission</button>
11 </form>
12 );
13}The form works like any React form - the types didn't add a single JSX attribute, but every handler is now checked by the compiler.
Functions as Props - Typing Callbacks
When a component receives a handler from its parent, you write the function type in the props interface. void means we don't care about the function's result:
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}My advice: don't guess the event type - hover over the onChange attribute in your editor and TypeScript shows the exact type to copy. In the lesson on generic components you'll use these handlers in a universal select field.
Code for this lesson: App.tsx
1import React, { useState } from 'react';
2
3// Mission form data interface
4interface MissionFormData {
5 name: string;
6 destination: string;
7 crewSize: number;
8 priority: "low" | "medium" | "high";
9 notes: string;
10}
11
12// Mission interface (with id and date)
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 // Typed form events
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>Space Mission Planning</h1>
69
70 <form onSubmit={handleSubmit} className="form">
71 <input name="name" value={formData.name} onChange={handleInputChange}
72 placeholder="Mission name" required />
73 <input name="destination" value={formData.destination} onChange={handleInputChange}
74 placeholder="Destination" 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">Low priority</option>
79 <option value="medium">Medium priority</option>
80 <option value="high">High priority</option>
81 </select>
82 <textarea value={formData.notes} onChange={handleTextareaChange}
83 placeholder="Notes..." rows={2} />
84 <button type="submit">Plan mission</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>Destination: {mission.destination}</p>
92 <p>Crew: {mission.crewSize} people</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)}>Remove</button>
99 </div>
100 ))}
101 {missions.length === 0 && <p className="empty">No planned missions</p>}
102 </div>
103 </div>
104 );
105}
106
107export default App;Remember: the event type tells you which ship console the signal came from - knowing the source, you know what data it carries.
Spotted a mistake in this lesson?
Check yourself
Answer the questions from this lesson. Pick an answer to see right away whether it is correct.
1. What event type should be used in the onChange handler for an <input> element?
Hands-on tasks in the game
- Vertical ordering
Arrange event types from the most general to the most specific:
- Code editor
Typing onChange and onSubmit events
- Click in order
Arrange the signature of a typed button click handler:
- Code editor
Typing mouse and keyboard events