Séance 2

Fonctions, objets et tableaux

Rémi Sharrock

TypeScript 101, Télécom Paris

Échauffement

Trois questions sur la séance 1, tirées des QCM du cours.

Question 1 (chapitre 1.1)

Qu’affiche ce code ?

const nom = "Alice";
console.log(`Bonjour $nom`);
  • A. Bonjour Alice
  • B. Bonjour $nom
  • C. Une erreur de compilation
  • D. Bonjour undefined

B. Sans accolades, $nom est du texte ordinaire : il faut ${nom}.

Question 2 (chapitre 1.2)

Ce code s’exécute sans erreur, et rien n’apparaît dans la page. Pourquoi ?

const item = document.createElement("li");
item.textContent = "Pomme";
  • A. textContent ne fonctionne pas sur un <li>
  • B. L’élément n’a jamais été inséré dans la page
  • C. Il fallait utiliser innerHTML
  • D. Il faut recharger la page

B. Sans appendChild ou prepend, l’élément reste détaché.

Question 3 (chapitre 1.3)

moduleMath.ts contient const pi = 3.14159;, sans export.

import { pi } from "./moduleMath.ts";
  • A. pi vaut 3.14159
  • B. Une erreur de compilation : le module n’exporte pas pi
  • C. pi vaut undefined
  • D. pi est importé en lecture seule

B. Ce qui ne porte pas export reste invisible hors du fichier.

Objectifs

  • Écrire des fonctions fléchées et les passer en paramètre
  • Distinguer corps d’instructions et corps d’expression
  • Décrire des objets de données et les copier sans effet de bord
  • Parcourir un tableau avec map, filter, find, reduce

Fonctions fléchées

Une fonction est une valeur

En C, une fonction est une entité à part. En TypeScript, c’est une valeur : elle se range dans une variable, se passe en paramètre, se renvoie en résultat.

function ajouter(a: number, b: number): number { return a + b; }

const ajouter2 = (a: number, b: number): number => a + b;

Trois mots pour la même chose : fonction fléchée, fonction anonyme, lambda. La syntaxe générale : (paramètres) => corps.

Corps d’instructions

const afficherSomme = (a: number, b: number): void => {
  const somme = a + b;
  console.log(`La somme est : ${somme}`);
};

afficherSomme(5, 3);   // La somme est : 8

Les accolades délimitent un bloc : plusieurs lignes, variables locales, if, boucles.

Rien n’est renvoyé automatiquement. Il faut écrire return.

Corps d’expression

const multiplier = (a: number, b: number): number => a * b;

console.log(multiplier(5, 3));   // 15

Une seule expression, dont la valeur est renvoyée implicitement.

Instructions Expression
Écriture => { ... } => expression
Lignes autant qu’on veut une seule
Retour explicite, return implicite
Sans return renvoie undefined impossible d’oublier

Le piège des accolades

const doubler = (x: number) => { x * 2 };   // renvoie undefined !

Les accolades ouvrent un bloc : x * 2 est calculé, puis jeté. Deux corrections :

const doubler = (x: number): number => x * 2;
const doubler2 = (x: number): number => { return x * 2; };

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

Une fonction en paramètre

const afficher = (
  a: number,
  b: number,
  operation: (a: number, b: number) => number
) => {
  console.log(`Résultat : ${operation(a, b)}`);
};

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

Lisez (a: number, b: number) => number comme une signature : « deux nombres en entrée, un nombre en sortie ». N’importe quelle fonction de cette forme convient.

Vous 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
  • vous pouvez écrire la fonction sur place, à l’endroit de l’appel
afficher(5, 3, ajouterVoid);   // ERREUR : void n'est pas assignable à number

Le compilateur vérifie la signature entière, pas seulement le nombre de paramètres.

Fonctions de rappel et temps

setTimeout(() => {
  console.log("4 secondes se sont écoulées");
}, 4000);

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

Durées en millisecondes. setInterval ne s’arrête jamais seul : gardez sa valeur de retour pour clearInterval.

setTimeout(maFonction(), 1000) appelle la fonction tout de suite. Écrivez setTimeout(maFonction, 1000), sans parenthèses. Même règle pour addEventListener.

Fermetures

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

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

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

C’est une fermeture (closure) : la fonction capture son environnement.

Objets de données

Nommer une forme

type Genre = "roman" | "essai" | "bd";   // type : n'importe quel type

interface Livre {                        // interface : la forme d'un objet
  titre: string;
  prix: number;
  genre: Genre;
}

const dune: Livre = { titre: "Dune", prix: 12.5, genre: "roman" };

Règle suffisante pour ce cours : interface pour un objet, type pour tout le reste (unions, littéraux, tuples).

Optionnel et readonly

interface Produit {
  readonly reference: string;   // fixée à la création
  nom: string;
  promotion?: number;           // peut être absente : number | undefined
}

const clavier: Produit = { reference: "KB-01", nom: "Clavier" };
clavier.reference = "KB-02";    // ERREUR : Cannot assign to 'reference'

const remise = (clavier.promotion ?? 0) * 0.5;   // ?? fournit une valeur de repli

const protège la variable, readonly protège la propriété.

Le typage structurel

Seule la forme compte, pas le nom.

function presenter(p: Personne): string { return `${p.nom}, ${p.age} ans`; }

const etudiant = { nom: "Eve", age: 22, ecole: "Télécom Paris" };
presenter(etudiant);   // accepté : etudiant a au moins nom et age

const eve: Personne = { nom: "Eve", age: 22, ville: "Paris" };
// ERREUR : Object literal may only specify known properties

Une propriété en trop n’est refusée que dans un littéral écrit à la main : c’est presque toujours une faute de frappe.

Copier en modifiant

const livre = { titre: "Dune", prix: 12.5 };

const alias = livre;                       // PAS une copie : le même objet
const promo = { ...livre, prix: 9.9 };     // un NOUVEL objet

console.log(livre.prix, promo.prix);       // 12.5 9.9
  • ...livre recopie toutes les propriétés, puis prix est remplacé
  • l’ordre compte : { prix: 9.9, ...livre } garde l’ancien prix
  • la copie est superficielle : les objets imbriqués restent partagés

Objets imbriqués

interface Client {
  nom: string;
  adresse: Adresse;   // un objet dans un objet
  achats: Livre[];    // un tableau d'objets
}

console.log(client.achats[1].titre);   // « le client, ses achats, le 2e, son titre »

Livre[] : un tableau d’objets, la forme de données la plus courante. C’est ce que la suite de la séance apprend à parcourir.

Vous venez du C ?

C TypeScript
struct Personne { ... }; interface Personne { ... }
struct Personne q = p; (copie) const q = { ...p };
struct Personne *r = &p; (même objet) const r = p;

En C, affecter une struct copie ses champs. En TypeScript, affecter un objet ne copie rien.

Parcourir un tableau

Décrire au lieu de dérouler

for (int i = 0; i < n; i++) { noms[i] = produits[i].nom; }

Trois occasions de se tromper (i, la borne, la taille de destination) pour une idée très simple.

const noms = produits.map((p) => p.nom);

Vous fournissez une fonction, le tableau se charge du parcours.

Le catalogue des exemples

interface Produit { nom: string; categorie: string; prix: number; stock: number; }

const produits: Produit[] = [
  { nom: "Clavier", categorie: "informatique", prix: 45, stock: 12 },
  { nom: "Souris", categorie: "informatique", prix: 20, stock: 0 },
  { nom: "Cahier", categorie: "papeterie", prix: 3, stock: 150 },
  { nom: "Stylo", categorie: "papeterie", prix: 1.5, stock: 300 },
  { nom: "Écran", categorie: "informatique", prix: 180, stock: 4 },
  { nom: "Agenda", categorie: "papeterie", prix: 12, stock: 0 },
];

Un tableau d’objets : la forme de données la plus courante, et celle des évaluations.

forEach et map

produits.forEach((p) => console.log(`${p.nom} : ${p.prix} €`));   // renvoie void

const noms: string[] = produits.map((p) => p.nom);         // Produit[] → string[]
const soldes: Produit[] = produits.map((p) => ({ ...p, prix: p.prix / 2 }));

map ne modifie pas l’original : une ligne map sans const à gauche est presque toujours un bug. Pour renvoyer un objet : ({ ... }), avec les parenthèses.

filter et find

const enStock: Produit[] = produits.filter((p) => p.stock > 0);   // 4 produits

const ecran: Produit | undefined = produits.find((p) => p.nom === "Écran");
if (ecran !== undefined) {
  console.log(ecran.prix);   // 180
}
  • filter : un tamis, les éléments retenus restent intacts
  • find : le premier qui convient, ou undefined

reduce

Vous parcourez le tableau avec un accumulateur dans la main.

const valeurStock = produits.reduce((acc, p) => acc + p.prix * p.stock, 0);   // 2160
Étape acc avant p acc après
départ 0
1 0 Clavier, 45 × 12 540
2 540 Souris, 20 × 0 540
3 540 Cahier, 3 × 150 990
… …
6 2160 Agenda, 12 × 0 2160

La valeur initiale 0 fixe le type de l’accumulateur.

Quelle méthode ?

Je veux obtenir…
├── rien : j'agis sur chaque élément            →  forEach
├── un tableau
│   ├── même longueur, éléments transformés     →  map
│   └── plus court, éléments intacts            →  filter
└── une seule valeur
    ├── un élément : le premier qui convient    →  find
    └── un résumé : somme, max, texte           →  reduce

Transformer : map. Garder : filter. Un seul : find. Résumer : reduce.

Lire une chaîne de méthodes

const resume = produits
  .filter((p) => p.categorie === "papeterie")
  .filter((p) => p.stock > 0)
  .map((p) => `${p.nom} (${p.prix} €)`)
  .join(", ");
Étape Renvoie Type
produits 6 produits Produit[]
.filter(papeterie) Cahier, Stylo, Agenda Produit[]
.filter(en stock) Cahier, Stylo Produit[]
.map(texte) ["Cahier (3 €)", "Stylo (1.5 €)"] string[]
.join(", ") "Cahier (3 €), Stylo (1.5 €)" string

Suivez le type d’un maillon à l’autre. En cas de doute, coupez la chaîne en constantes et affichez-les.

Les pièges

Piège Symptôme
find sans tester undefined 'x' is possibly 'undefined'
reduce sans valeur initiale l’accumulateur devient un Produit
map((p) => { p.prix * 2; }) void[] : il manque le return
const r = tab.forEach(...) r vaut undefined
`${produits}` [object Object],[object Object],...

Pour afficher un tableau d’objets : produits.map((p) => p.nom).join(", ").

À vous : par où commencer

Parcours essentiel, dans l’ordre

  • 2.1 Coordonnées du clic
  • 2.2 Calculatrice
  • 2.4 Décrire une fiche de film
  • 2.7 Des fruits, des fruits, des fruits
  • 2.8 Choisir la bonne méthode

Ensuite

  • 2.3 Position de la souris, une fois par seconde (entraînement, transfert)
  • 2.5 Propriété optionnelle et readonly (entraînement)
  • 2.6 Une remise sans effet de bord (entraînement, transfert)
  • 2.9 Prédire avant d’exécuter (entraînement)
  • 2.10 Statistiques sur un catalogue (entraînement, transfert)
  • 2.11 Enchaînement et affichage dans la page (approfondissement)

Les exercices « transfert » sont les plus proches de ce que demandent les évaluations.