2.1 · Fonctions fléchées

Les diapositives de la séance 2

  • écrire une fonction fléchée sous sa forme longue et sous sa forme courte
  • distinguer un corps d’instructions d’un corps d’expression
  • reconnaître le retour implicite et le piège des accolades
  • typer une fonction reçue en paramètre
  • utiliser des fonctions de rappel avec setTimeout, setInterval et addEventListener

Les types de base et les template literals : 1.1 · Hello world TypeScript. Les écouteurs d’événements : 1.2 · Une page web avec Astro.

Le code de ce chapitre : télécharger app.ts. Il ne touche pas au document, vous pouvez donc l’exécuter directement dans un terminal :

npx tsx app.ts

Une fonction rangée dans une variable

En C, une fonction est une entité à part : elle a un nom, elle vit au niveau du fichier, on ne peut pas la créer au milieu d’un calcul. En TypeScript, une fonction est une valeur comme une autre : elle se range dans une variable, se passe en paramètre, se renvoie en résultat.

// Déclaration classique
function ajouter(a: number, b: number): number { return a + b; }

// Même chose, en fonction fléchée rangée dans une constante
const ajouter2 = (a: number, b: number): number => a + b;

On dit fonction fléchée (à cause de =>), fonction anonyme (elle n’a pas de nom propre, seule la constante en a un) ou lambda. Ce sont trois mots pour la même chose.

La syntaxe générale :

(paramètres) => corps

Instructions ou expression : la vraie différence

Le corps d’une fonction fléchée s’écrit de deux façons, et c’est la distinction centrale du chapitre.

Corps d’instructions : avec accolades

app.ts
const ajouter = (a: number, b: number): void => {
  const somme = a + b;
  console.log(`La somme est: ${somme}`);
};
ajouter(5, 3);   // affiche "La somme est: 8"

Les accolades délimitent un bloc : plusieurs lignes, des variables locales, des if, des boucles. Rien n’est renvoyé automatiquement : il faut écrire return. Ici la fonction ne renvoie rien, d’où le type de retour void.

Corps d’expression : sans accolades

app.ts
const multiplier = (a: number, b: number): number => a * b;
console.log(multiplier(5, 3));   // affiche 15

Une seule expression, dont la valeur est renvoyée implicitement. Pas d’accolades, pas de return, pas de point-virgule interne.

Corps d’instructions Corps d’expression
Écriture => { ... } => expression
Nombre de lignes autant qu’on veut une seule
Retour explicite, avec return implicite
Sans return renvoie undefined impossible d’oublier
Bon pour une logique, des cas, des effets une transformation courte
const doubler = (x: number) => { x * 2 };   // renvoie undefined !

Les accolades ouvrent un bloc d’instructions : x * 2 y est calculé puis jeté. Le compilateur ne s’en plaint pas si vous n’avez pas annoté le type de retour : il en déduit void. Deux corrections, au choix :

const doubler = (x: number): number => x * 2;          // on enlève les accolades
const doubler2 = (x: number): number => { return x * 2; };  // on ajoute return

Annoter le type de retour transforme cette erreur silencieuse en erreur de compilation. C’est la meilleure raison de le faire.

NoteRenvoyer un objet en forme courte

() => { nom: "Alice" } ne renvoie pas un objet : les accolades sont lues comme un bloc. Entourez l’objet de parenthèses : () => ({ nom: "Alice" }). Le sujet revient au chapitre 2.2 · Objets de données.

La liste de paramètres

const rien = () => console.log("aucun paramètre");   // parenthèses vides obligatoires
const carre = (x: number): number => x * x;

En JavaScript, les parenthèses sont facultatives pour un paramètre unique. En TypeScript, gardez-les toujours : elles sont obligatoires dès qu’il y a une annotation de type, et le typage est précisément ce qui vous protège.

Une fonction comme paramètre

C’est l’usage qui justifie tout le reste. Le type d’un paramètre qui est une fonction s’écrit avec la même flèche :

app.ts
const afficher = (
  a: number,
  b: number,
  operation: (a: number, b: number) => number   // ← le type : deux number, renvoie un number
) => {
  console.log(`Le résultat est: ${operation(a, b)}`);
};

afficher(5, 3, multiplier);                        // 15
afficher(5, 3, (a: number, b: number) => a / b);   // 1.6666666666666667
afficher(5, 3, Math.pow);                          // 125

Lisez (a: number, b: number) => number comme une signature : « une fonction qui prend deux nombres et rend un nombre ». N’importe quelle fonction de cette forme convient, quel que soit son nom.

afficher(5, 3, ajouter);   // ERREUR : ajouter renvoie void, pas number

Le message parle de void non assignable à number. Le compilateur vérifie la signature entière, pas seulement le nombre de paramètres.

Rien n’empêche de passer une fonction à corps d’instructions, du moment qu’elle renvoie bien un number :

app.ts
afficher(5, 3, (a: number, b: number) => {
  let resultat: string = "";
  for (let i = 0; i < a; i++) {
    resultat += b;
  }
  return parseInt(resultat);
});
// Le résultat est: 33333
NoteVous venez du C ?

int (*op)(int, int) est l’ancêtre direct de (a: number, b: number) => number. Deux différences : la notation TypeScript se lit de gauche à droite sans acrobatie, et surtout vous pouvez écrire la fonction sur place, à l’endroit de l’appel, au lieu de la déclarer ailleurs dans le fichier.

Fonctions de rappel et temps

setTimeout et setInterval reçoivent une fonction et décident quand l’appeler. Vous ne l’appelez jamais vous-même : c’est le principe d’une fonction de rappel (callback).

app.ts
// Une seule fois, après 4 secondes
setTimeout(() => {
  console.log("4 secondes se sont écoulées");
}, 4000);

// Toutes les 9 secondes, indéfiniment
setInterval(() => {
  console.log("répétition: 9 secondes se sont écoulées");
}, 9000);

Les durées sont en millisecondes. setInterval ne s’arrête jamais tout seul : conservez la valeur qu’il renvoie pour pouvoir l’interrompre avec clearInterval(identifiant).

setTimeout(afficherVariables(), 1000) appelle la fonction immédiatement et passe son résultat à setTimeout. Écrivez setTimeout(afficherVariables, 1000), sans parenthèses : on passe la fonction, on ne l’exécute pas. Même règle pour addEventListener.

Accéder aux variables extérieures

Une fonction fléchée voit les variables déclarées autour d’elle, au moment où elle s’exécute :

app.ts
let x = 5;
const afficherVariables = () => {
  console.log(`x: ${x}`);
};
x = 42;
afficherVariables();   // affiche "x: 42", pas "x: 5"

Cette capture de l’environnement s’appelle une fermeture (closure). Elle permet de fabriquer des fonctions sur mesure :

const saluer = (nom: string) => {
  return () => console.log(`Bonjour, ${nom}!`);
};

const saluerAlice = saluer("Alice");   // rien ne s'affiche encore
saluerAlice();                         // affiche "Bonjour, Alice!"

saluer ne salue personne : elle fabrique une fonction qui, elle, saluera. La valeur de nom reste attachée à la fonction produite.

Exercices

Exercice 2.1 : Coordonnées du clic

essentiel

Dans un projet Astro, faites charger app.ts par une page pour disposer de document, et ajoutez :

document.addEventListener("click", (event: MouseEvent) => {
  console.log(`x: ${event.clientX}, y: ${event.clientY}`);
});

Le paramètre event n’est pas fourni par vous : le navigateur le passe à chaque clic.

  1. Vérifiez dans la console que les coordonnées changent d’un clic à l’autre.
  2. Remplacez clientX par pageX et cliquez après avoir fait défiler la page. Que change ce choix ?

Réussite : vous savez expliquer qui appelle la fonction fléchée, et d’où vient son paramètre.

src/pages/index.astro
---
---
<html lang="fr">
<head>
    <meta charset="UTF-8">
    <title>Coordonnées du clic</title>
</head>
<body style="height: 3000px">
    <h1>Cliquez n'importe où, puis faites défiler et recliquez</h1>
    <script src="../scripts/app.ts"></script>
</body>
</html>

La page est volontairement très haute : sans défilement possible, la question 2 ne montre rien.

src/scripts/app.ts
document.addEventListener("click", (event: MouseEvent) => {
  console.log(`client x: ${event.clientX}, y: ${event.clientY}`);
  console.log(`page   x: ${event.pageX}, y: ${event.pageY}`);
});

2. Tant que la page n’a pas défilé, les deux couples de valeurs sont identiques. Défilez de 500 pixels, cliquez au même endroit de l’écran : clientY n’a pas bougé, pageY a augmenté de 500.

  • clientX / clientY : position par rapport à la fenêtre visible. L’origine est le coin haut-gauche de ce que vous voyez, et elle se déplace avec le défilement.
  • pageX / pageY : position par rapport au document entier. L’origine est le haut du document, et elle ne bouge jamais.

La relation est simple : pageY === clientY + window.scrollY.

Le choix découle de l’usage. Pour positionner une infobulle ou un menu contextuel qui suit le curseur à l’écran, clientX/clientY ; pour mémoriser « l’utilisateur a cliqué à cet endroit du document » et le retrouver plus tard, pageX/pageY.

Le point clé : la question « qui appelle cette fonction ? » est le cœur du chapitre. Vous n’appelez jamais cette fonction fléchée ; vous la confiez au navigateur, qui la garde de côté et l’appellera lui-même à chaque clic, en lui passant un objet MouseEvent décrivant ce qui vient de se produire. C’est un renversement du flux de contrôle : votre code ne décide plus du quand, il décrit seulement le quoi.

C’est aussi ce qui explique que la fonction soit anonyme : elle n’a pas de nom parce que vous n’aurez jamais à l’appeler par son nom.

Le MouseEvent contient bien plus que des coordonnées : event.target (l’élément cliqué), event.button (quel bouton de la souris), event.shiftKey (la touche Maj était-elle enfoncée). L’annotation : MouseEvent vous donne accès à tout cela dans l’autocomplétion : avec un simple : Event, clientX serait refusé, car tous les événements n’ont pas de coordonnées.

Exercice 2.2 : Calculatrice

essentiel

  1. Écrivez quatre fonctions fléchées additionner, soustraire, multiplier, diviser, chacune en corps d’expression (une ligne, sans accolades ni return).
  2. Écrivez calculatrice(a: number, b: number, operation: string) en corps d’instructions, qui choisit la bonne fonction et affiche le résultat. calculatrice(5, 3, "additionner") doit afficher La somme est: 8.
  3. Gérez l’opération inconnue et la division par zéro.

Réussite : les quatre opérations tiennent chacune sur une ligne, et calculatrice(5, 0, "diviser") n’affiche pas Infinity.

Un switch sur operation, ou mieux : un type littéral "additionner" | "soustraire" | "multiplier" | "diviser" à la place de string, et l’éditeur vous proposera les quatre valeurs.

calculatrice.ts
// 1. Les quatre opérations, en corps d'expression : une ligne, sans accolades
const additionner = (a: number, b: number): number => a + b;
const soustraire = (a: number, b: number): number => a - b;
const multiplier = (a: number, b: number): number => a * b;
const diviser = (a: number, b: number): number => a / b;

// Le type littéral remplace string : seules ces quatre chaînes sont acceptées
type Operation = "additionner" | "soustraire" | "multiplier" | "diviser";

// 2. et 3. La calculatrice, en corps d'instructions
const calculatrice = (a: number, b: number, operation: Operation): void => {
  // 3. Division par zéro : on traite le cas AVANT de calculer
  if (operation === "diviser" && b === 0) {
    console.log("Division par zéro impossible");
    return;
  }

  switch (operation) {
    case "additionner":
      console.log(`La somme est: ${additionner(a, b)}`);
      break;
    case "soustraire":
      console.log(`La différence est: ${soustraire(a, b)}`);
      break;
    case "multiplier":
      console.log(`Le produit est: ${multiplier(a, b)}`);
      break;
    case "diviser":
      console.log(`Le quotient est: ${diviser(a, b)}`);
      break;
    default:
      console.log(`Opération inconnue: ${operation}`);
  }
};

calculatrice(5, 3, "additionner");   // La somme est: 8
calculatrice(10, 4, "soustraire");   // La différence est: 6
calculatrice(6, 7, "multiplier");    // Le produit est: 42
calculatrice(15, 4, "diviser");      // Le quotient est: 3.75
calculatrice(5, 0, "diviser");       // Division par zéro impossible

Le point clé : la division par zéro est le véritable sujet de l’exercice. 5 / 0 ne provoque aucune erreur en JavaScript : le résultat est Infinity, une valeur parfaitement légitime de type number. Le compilateur ne dira rien, l’exécution non plus, et la valeur va se propager tranquillement dans la suite de vos calculs : Infinity - Infinity donnant NaN, qui contamine ensuite tout ce qu’il touche. C’est un piège bien pire qu’un plantage : rien ne signale l’anomalie. Le test explicite sur b === 0 est donc à votre charge, personne ne le fera à votre place.

Sur le type littéral : avec operation: string, calculatrice(5, 3, "addition") compile sans broncher et affiche Opération inconnue à l’exécution. Avec le type Operation, la faute de frappe devient une erreur de compilation, et l’éditeur propose les quatre valeurs valides dès la première lettre tapée. Le default du switch devient d’ailleurs inatteignable. Gardez-le tout de même : il reste le filet de sécurité si la valeur vient d’un formulaire ou d’une API, où les types ne protègent plus rien.

Alternative : un objet qui associe chaque nom à sa fonction supprime le switch :

const operations = { additionner, soustraire, multiplier, diviser };
console.log(operations["multiplier"](6, 7));   // 42

C’est plus court et se prête mieux à l’ajout d’opérations, mais on perd le message personnalisé par opération (« somme », « produit »…) que demande l’énoncé. Les objets sont le sujet du chapitre 2.2 · Objets de données.

L’erreur classique : oublier un break. Sans lui, l’exécution continue dans le case suivant et calculatrice(5, 3, "additionner") affiche la somme puis la différence.

Exercice 2.3 : Position de la souris, une fois par seconde

entraînement transfert

Affichez les coordonnées de la souris toutes les secondes, et non à chaque mouvement.

Le piège : mousemove se déclenche des dizaines de fois par seconde, setInterval n’a pas accès à l’événement. Il faut donc combiner les deux : mémoriser la dernière position connue dans des variables extérieures, et les lire depuis l’intervalle.

Réussite : une ligne par seconde dans la console, même si la souris bouge sans arrêt, et rien de nouveau si elle ne bouge plus.

src/scripts/app.ts
// Variables extérieures : elles servent de boîte aux lettres entre les deux fonctions
let dernierX: number = 0;
let dernierY: number = 0;
let aBouge: boolean = false;

// L'écrivain : appelé des dizaines de fois par seconde, il ne fait qu'enregistrer
document.addEventListener("mousemove", (event: MouseEvent) => {
  dernierX = event.clientX;
  dernierY = event.clientY;
  aBouge = true;
});

// Le lecteur : appelé une fois par seconde, il affiche et remet le drapeau à zéro
setInterval(() => {
  if (aBouge) {
    console.log(`Position de la souris : x: ${dernierX}, y: ${dernierY}`);
    aBouge = false;
  }
}, 1000);

Le point clé : deux fonctions de rappel, appelées par deux déclencheurs différents et à des rythmes sans rapport, qui communiquent uniquement par des variables déclarées à l’extérieur des deux. C’est la seule solution possible, et pour une raison de fond : setInterval n’a aucun moyen d’accéder à un MouseEvent, puisque son appel n’est déclenché par aucun mouvement de souris. L’information doit donc être mémorisée au moment où elle est disponible, pour être relue plus tard.

Les deux fonctions voient dernierX et dernierY par fermeture : une fonction fléchée capture les variables de son environnement de déclaration et lit leur valeur au moment où elle s’exécute, pas au moment où elle est écrite. C’est exactement l’exemple x = 42 vu plus haut dans le chapitre.

C’est aussi le seul endroit du corrigé où let s’impose : ces trois valeurs changent en permanence, const les rendrait inutilisables.

Sur le drapeau aBouge : sans lui, l’intervalle réafficherait la même position chaque seconde, souris immobile comprise, et noierait la console. C’est ce que demande la dernière condition de réussite. Le motif porte un nom, throttling (limitation de fréquence) : on reçoit tous les événements, on n’en traite qu’un par période. Il est indispensable dès que la réaction à un événement coûte cher (requête réseau, redécoupage de la page), car mousemove, scroll et resize se déclenchent facilement soixante fois par seconde.

Pour bien faire : setInterval renvoie un identifiant qu’il faut conserver si l’on veut pouvoir arrêter la répétition :

const timer = setInterval(() => { /* … */ }, 1000);
// plus tard : clearInterval(timer);

QCM

1. Dans une fonction fléchée, qu’est-ce qui distingue un corps d’expression d’un corps d’instructions ?

A. Ils sont interchangeables : seule la présence du mot-clé return les distingue. B. L’expression s’écrit sans accolades et sa valeur est renvoyée ; les instructions exigent des accolades et ne renvoient rien automatiquement. C. Les expressions sont interdites dans les fonctions fléchées. D. Il n’y a aucune différence.

B. Les accolades ouvrent un bloc : sans return, la fonction renvoie undefined.

2. Une lambda à corps d’instructions, par rapport à une lambda à corps d’expression :

A. ne peut pas renvoyer de valeur. B. utilise des accolades et peut contenir plusieurs instructions, alors que la forme à expression renvoie directement le résultat d’une expression unique. C. ne peut pas avoir de paramètres. D. ne peut pas être rangée dans une const.

B. La forme à instructions peut tout à fait renvoyer une valeur, à condition d’écrire return.

3. Qu’affiche ce code ?

setTimeout(() => {
  console.log("4");
}, 4000);

setInterval(() => {
  console.log("9");
}, 9000);

A. « 4 » une fois après 4 secondes, « 9 » une fois après 4 secondes. B. « 4 » une fois après 4 secondes, « 9 » toutes les 9 secondes. C. « 4 » toutes les 4 secondes, « 9 » une fois après 9 secondes. D. « 4 » toutes les 4 secondes, « 9 » toutes les 9 secondes.

B. setTimeout déclenche une seule fois ; setInterval répète jusqu’à clearInterval.

4. Que fait saluer ?

const saluer = (nom: string) => {
  return () => console.log(`Bonjour, ${nom}!`);
};

A. Elle affiche un message de salutation. B. Elle renvoie le message de salutation sous forme de chaîne. C. Elle renvoie une fonction qui prend un nom en paramètre. D. Elle renvoie une fonction qui affiche un message de salutation.

D. saluer("Alice") n’affiche rien : elle produit une fonction. C’est l’appel de cette fonction qui déclenche l’affichage.

À retenir

  • (params) => expression renvoie la valeur ; (params) => { ... } exige un return
  • Annoter le type de retour transforme l’oubli de return en erreur de compilation
  • Une fonction est une valeur : elle se range, se passe et se renvoie
  • Le type d’une fonction s’écrit (a: number, b: number) => number
  • On passe une fonction de rappel (maFonction), on ne l’appelle pas (maFonction())
  • Une fonction fléchée voit les variables déclarées autour d’elle : c’est une fermeture