Cours JavaScript et React · Module 12: React et TypeScript

Typer les événements - ChangeEvent, FormEvent, MouseEvent

6 min de lecture
Dans cette leçon6

Tu sors un gestionnaire de clic du JSX, tu écris (event) => ... et soudain l'éditeur ne sait plus rien de event : ni qu'il possède un champ value, ni qu'on peut appeler preventDefault(). Les événements en React sont des signaux provenant de différentes parties du vaisseau spatial - clics sur des boutons, saisie de données, soumission de formulaires. TypeScript te permet de définir précisément quel signal tu reçois et de quelle source.

Bonne nouvelle pour commencer : quand tu écris une fonction directement dans l'attribut, par exemple onChange={e => ...}, TypeScript déduit lui-même le type de l'événement. Un type explicite n'est nécessaire que lorsque le gestionnaire est une fonction séparée - et c'est ainsi que sont écrits la plupart des vrais composants.

Types d'événements de base

React fournit ses propres types d'événements, qui sont génériques - entre chevrons, tu indiques de quel élément HTML provient l'événement. Ils dérivent tous du type de base React.SyntheticEvent, l'enveloppe que React place autour de l'événement natif du navigateur. Les plus courants sont MouseEvent (souris), ChangeEvent (changement de valeur d'un champ) et SubmitEvent (soumission d'un formulaire) :

1import React from 'react';
2
3// Événement de clic
4const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
5  console.log("Button clicked:", event.currentTarget.textContent);
6};
7
8// Événement de changement de valeur
9const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
10  console.log("New value:", event.target.value);
11};
12
13// Événement de soumission de formulaire
14const handleSubmit = (event: React.SubmitEvent<HTMLFormElement>) => {
15  event.preventDefault();
16  console.log("Form submitted!");
17};

Le paramètre entre chevrons est l'élément auquel le gestionnaire est attaché. Grâce à lui, event.currentTarget dans le premier gestionnaire a le type HTMLButtonElement, et event.target.value dans le deuxième est une chaîne. La méthode preventDefault() empêche le rechargement de la page par défaut après la soumission d'un formulaire.

FormEvent - l'ancien nom

Pendant des années, on a typé les formulaires avec React.FormEvent, et c'est ainsi qu'est écrite une grande partie du code existant, tout comme le titre de cette leçon. Depuis @types/react 19.2.10 (janvier 2026), FormEvent est marqué comme obsolète (deprecated), car il ne correspond à aucun véritable événement du navigateur. Utilise plutôt SubmitEvent pour onSubmit, ChangeEvent pour onChange, ou le type général SyntheticEvent.

1// Ancienne notation - compile toujours, mais est obsolète
2const handleSubmitLegacy = (event: React.FormEvent<HTMLFormElement>) => {
3  event.preventDefault();
4};

Ce code fonctionne et compile toujours, l'éditeur se contente de barrer le nom du type. Si ton projet utilise des types antérieurs à 19.2.10, SubmitEvent n'existera pas encore - garde alors FormEvent et mets le paquet à jour dès que possible.

ChangeEvent - différents éléments de formulaire

Le type ChangeEvent varie selon l'élément que tu écoutes. Un champ texte, une zone de texte et une liste déroulante ont tous une value de type string, et une case à cocher a en plus checked de type boolean :

1// Champ texte
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};

Remarque qu'une case à cocher est aussi un HTMLInputElement - seul le champ que tu lis change. Attention : value est toujours une chaîne, même dans <input type="number">. Tu obtiens un nombre avec Number(e.target.value) ou e.target.valueAsNumber.

MouseEvent - clics et mouvements de souris

MouseEvent transporte les coordonnées du curseur (clientX, clientY) et des informations sur l'élément. Le même type gère un clic comme un survol :

1// Clic sur un bouton
2const handleButtonClick = (e: React.MouseEvent<HTMLButtonElement>) => {
3  console.log("Click position:", e.clientX, e.clientY);
4};
5
6// Clic sur un div
7const handleDivClick = (e: React.MouseEvent<HTMLDivElement>) => {
8  const target = e.currentTarget; // HTMLDivElement
9};
10
11// Survol de la souris
12const handleMouseEnter = (e: React.MouseEvent<HTMLDivElement>) => {
13  console.log("Mouse entered the element");
14};

currentTarget est toujours l'élément qui porte le gestionnaire, il a donc un type précis. target, lors d'un clic, peut être n'importe quel enfant à l'intérieur, donc TypeScript le type de façon générale.

KeyboardEvent - événements clavier

Pour le clavier, tu utilises KeyboardEvent, et tu lis la touche pressée dans le champ key :

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

Le champ key contient un nom de touche lisible, comme "Enter" ou "a". L'ancien champ keyCode est obsolète, ne l'utilise donc pas dans du nouveau code.

Exemple pratique - formulaire typé

Assemblons tout dans un formulaire de planification de mission. D'abord le type des données du formulaire, puis l'état typé avec un générique, comme dans la leçon précédente :

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

Maintenant, trois gestionnaires. La notation [name]: value met à jour le champ dont le nom vient de l'attribut name du champ de saisie. Le mot-clé as est une assertion de type : on affirme au compilateur que le texte de la liste est forcément l'une des trois priorités, puisque nous avons défini les options nous-mêmes. Utilise as avec parcimonie - il désactive la vérification à cet endroit.

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

Enfin, le JSX où l'on branche les gestionnaires. Le type d'élément de chaque gestionnaire doit correspondre à l'élément du JSX - un gestionnaire pour select ne sera pas accepté par un 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}

Le formulaire fonctionne comme n'importe quel formulaire React - les types n'ont ajouté aucun attribut au JSX, mais chaque gestionnaire est désormais vérifié par le compilateur.

Les fonctions comme props - typer les callbacks

Quand un composant reçoit un gestionnaire de son parent, tu écris le type de la fonction dans l'interface des props. void signifie que le résultat de la fonction ne nous intéresse pas :

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}

Mon conseil : ne devine pas le type d'événement - survole l'attribut onChange dans ton éditeur et TypeScript t'affiche le type exact à copier. Dans la leçon sur les composants génériques, tu utiliseras ces gestionnaires dans un champ de sélection universel.

Code de cette leçon : App.tsx
1import React, { useState } from 'react';
2
3// Interface des données du formulaire de mission
4interface MissionFormData {
5  name: string;
6  destination: string;
7  crewSize: number;
8  priority: "low" | "medium" | "high";
9  notes: string;
10}
11
12// Interface de mission (avec id et 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  // Événements de formulaire typés
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>Planification des missions spatiales</h1>
69
70      <form onSubmit={handleSubmit} className="form">
71        <input name="name" value={formData.name} onChange={handleInputChange}
72          placeholder="Nom de la mission" required />
73        <input name="destination" value={formData.destination} onChange={handleInputChange}
74          placeholder="Destination de la mission" 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">Priorité basse</option>
79          <option value="medium">Priorité moyenne</option>
80          <option value="high">Priorité haute</option>
81        </select>
82        <textarea value={formData.notes} onChange={handleTextareaChange}
83          placeholder="Notes..." rows={2} />
84        <button type="submit">Planifier la 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>Équipage : {mission.crewSize} pers.</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)}>Supprimer</button>
99          </div>
100        ))}
101        {missions.length === 0 && <p className="empty">Aucune mission planifiée</p>}
102      </div>
103    </div>
104  );
105}
106
107export default App;

Retiens : le type d'événement t'indique de quel pupitre du vaisseau vient le signal - en connaissant la source, tu sais quelles données il transporte.

Tu as repéré une erreur dans cette leçon ?

Teste tes connaissances

Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.

  1. 1. Quel type d'événement faut-il utiliser dans le gestionnaire onChange d'un élément <input> ?

Tâches pratiques dans le jeu

  • Tri vertical

    Ordonne les types d'événements du plus général au plus spécifique :

  • Éditeur de code

    Typer les événements onChange et onSubmit

  • Clique dans l'ordre

    Ordonne la signature d'un gestionnaire de clic de bouton typé :

  • Éditeur de code

    Typer les événements de la souris et du clavier

Articles utiles