Séance 5

Données et erreurs

Rémi Sharrock

TypeScript 101, Télécom Paris

Retour sur l’évaluation 1

Ce que vous recevez

  • votre note et un corrigé commenté, qui explique le pourquoi de chaque réponse
  • la grille de compétences de l’évaluation 1, sur sa page : chaque ligne renvoie à un ou deux exercices

Pour chaque question ratée : retrouvez sa ligne dans la grille, puis refaites l’exercice sans ouvrir le corrigé.

L’évaluation 2 porte sur tout le cours : ce qui a manqué ici resservira.

Les pièges qui coûtent des points

Erreur À refaire
find utilisé sans traiter le cas undefined 2.7, 2.8
Une chaîne de méthodes écrite d’un bloc, sans suivre le type 2.9
reduce sans valeur initiale 2.8
Un tableau d’objets affiché avec ${tableau} 2.7
this oublié dans une méthode 3.1
Un champ laissé public alors qu’un invariant exigeait private 3.2

Au tableau : une chaîne de méthodes

interface Livre { titre: string; prix: number; disponible: boolean; }

const livres: Livre[] = [
  { titre: "Dune", prix: 12, disponible: true },
  { titre: "Fondation", prix: 18, disponible: true },
  { titre: "Hypérion", prix: 9, disponible: false },
  { titre: "Solaris", prix: 8, disponible: true },
];

« Le prix total des livres disponibles à moins de 15 €. »

const total = livres
  .filter((l) => l.disponible)            // Dune, Fondation, Solaris : Livre[]
  .filter((l) => l.prix < 15)             // Dune, Solaris : Livre[]
  .reduce((acc, l) => acc + l.prix, 0);   // 20 : number

Une étape, une méthode ; notez le type et les valeurs à chaque maillon.

Au tableau : une classe

class Compteur {
  private valeur = 0;
  constructor(private max: number) {}

  incrementer(): void {
    if (valeur < max) valeur++;   // ERREUR : Cannot find name 'valeur'
  }
}
  incrementer(): void {
    if (this.valeur < this.max) this.valeur++;
  }

Grâce à private, valeur ne dépasse jamais max : il suffit de relire la classe pour le vérifier.

Objectifs

  • Sérialiser et désérialiser du JSON
  • Comprendre pourquoi JSON.parse renvoie any, et s’en protéger
  • Écrire un prédicat de type pour valider une donnée extérieure
  • Factoriser du code avec les generics <T>
  • Lever et rattraper des exceptions proprement

JSON

Le format

JavaScript Object Notation : un format texte d’échange de données, indépendant de tout langage.

{
  "id": "1042",
  "name": "Joe",
  "scores": [31.4, 29.9],
  "actif": true,
  "surnom": null
}

Six types de valeurs : chaîne, nombre, booléen, null, tableau, objet. Pas de fonctions, pas de méthodes, pas de Date : des données inertes.

Trois règles qui font trébucher : clés entre guillemets doubles, pas de virgule finale, pas de commentaires.

Deux opérations symétriques… en apparence

Opération Nom Sens
JSON.stringify(obj) sérialisation objet TypeScript → chaîne
JSON.parse(str) désérialisation chaîne → valeur JavaScript
const texte = JSON.stringify({ x: 3, y: 4 });   // {"x":3,"y":4}
const relu = JSON.parse(texte);

En sérialisant, vous partez d’une valeur dont TypeScript connaît le type : aucun risque. En désérialisant, vous partez d’une chaîne venue de l’extérieur : TypeScript ne sait rien.

Ce qui disparaît silencieusement

const objet = {
  nom: "Alice",
  saluer: () => console.log("hello"),   // fonction : ignorée
  absent: undefined,                    // undefined : ignoré
  date: new Date(),                     // converti en chaîne ISO
};

console.log(JSON.stringify(objet));
// {"nom":"Alice","date":"2026-09-11T10:00:00.000Z"}

Aucune erreur, aucun avertissement. Les propriétés ont juste disparu.

Le troisième paramètre indente, et c’est très utile pour déboguer : JSON.stringify(obj, null, 2).

Le piège de any

JSON.parse(text: string): any

Elle renvoie any : TypeScript renonce à vérifier quoi que ce soit.

const data = JSON.parse('{"name":"Joe"}');
console.log(data.nom.toUpperCase());   // compile sans broncher
// TypeError: Cannot read properties of undefined

C’est exactement le scénario que TypeScript est censé éviter, sauf qu’ici, vous avez ouvert la porte vous-même.

Annoter avec une interface

interface UserInterface {
  id: string;
  name: string;
  scores: number[];
}

const users: UserInterface[] = JSON.parse(data);
users.forEach((u) => console.log(`${u.id} : ${u.name}`));

C’est ce que fait 90 % du code TypeScript en production.

Comprenez ce que vous venez de faire : vous avez affirmé que le JSON avait cette forme. Si le serveur change demain, rien ne vous préviendra.

unknown + prédicat de type

function isServerData(value: unknown): value is ServerData {
  return (
    typeof value === "object" && value !== null &&
    "number" in value && typeof value.number === "number" &&
    "numbers" in value && Array.isArray(value.numbers)
  );
}
const parsed: unknown = JSON.parse(json);   // on force unknown, pas any

if (isServerData(parsed)) {
  console.log(parsed.number);   // ici, et seulement ici, le type est garanti
}

value is ServerData dit au compilateur : « si je renvoie true, tu peux considérer que la valeur a ce type ».

Trois niveaux de sûreté

Approche Sûreté à l’exécution Effort Quand
JSON.parse(x) seul aucune nul jamais
const u: User = JSON.parse(x) aucune faible TP, source de confiance
unknown + prédicat réelle moyen API externe, production

En production on utilise Zod, Valibot ou io-ts : ces bibliothèques génèrent le type et le validateur à partir d’une seule déclaration. Le principe reste le même.

Les méthodes perdues

JSON.parse restitue des données, jamais des instances de classe.

interface IUser {
  id: string;
  getMaxScore(): number;   // déclarée dans l'interface
}

const relus: IUser[] = JSON.parse(json);
relus[0].getMaxScore();
// Compile. À l'exécution : TypeError: getMaxScore is not a function

Une annotation ne crée rien

Vous avez promis une méthode au compilateur ; il vous a cru. Le moteur JavaScript, lui, n’a reçu que des propriétés.

Ne mettez jamais de méthode dans une interface destinée à typer un JSON.parse.

Reconstruire des instances

Séparez les deux rôles : une interface de données pour le transport, une classe pour le comportement.

interface UserData {          // uniquement des données
  id: string;
  name: string;
  scores: number[];
}

function toUsers(data: UserData[]): User[] {
  return data.map((d) => new User(d.id, d.name, d.scores));
}

const users: User[] = toUsers(JSON.parse(json));
console.log(users[0].getMaxScore());   // fonctionne

Generics

Le problème

Une fonction qui renvoie le premier élément d’un tableau. Deux mauvaises solutions.

Dupliquer :

function premierNombre(t: number[]): number | undefined { return t[0]; }
function premiereChaine(t: string[]): string | undefined { return t[0]; }

Utiliser any :

function premier(t: any[]): any { return t[0]; }
premier([1, 2, 3]).toUpperCase();   // compile, plante à l'exécution

Or il existe un lien évident : un tableau de number rend un number.

Un paramètre qui est un type

function premier<T>(tab: T[]): T | undefined {
  return tab[0];
}

Lisez à voix haute : « pour n’importe quel type T, premier prend un tableau de T et renvoie un T ou undefined ».

Le <T> déclare le paramètre de type, exactement comme les parenthèses déclarent les paramètres de valeur.

Conventions : T pour Type, K pour Key, V pour Value, E pour Element.

L’inférence fait le travail

const a = premier([1, 2, 3]);              // T = number
const b = premier(["x", "y"]);             // T = string
const c = premier<boolean>([true, false]); // T forcé explicitement

a?.toFixed(2);      // OK
a?.toUpperCase();   // ERREUR : n'existe pas sur type 'number'

Ne précisez <...> que lorsque l’inférence échoue ou produit un type trop large.

Interface et classe génériques

interface Resultat<T> {
  succes: boolean;
  donnee: T;
}
class GenericStorage<T> {
  private data: Map<string, T> = new Map();
  set(key: string, value: T): void { this.data.set(key, value); }
  get(key: string): T | undefined { return this.data.get(key); }
}

const nombres = new GenericStorage<number>();
nombres.set("one", 1);
// nombres.set("three", "3");   // ERREUR : string n'est pas number

Deux instances, deux types, une seule classe écrite.

Contraindre avec extends

<T> seul signifie « n’importe quoi », donc vous ne pouvez rien en faire.

function afficherLongueur<T>(x: T): void {
  console.log(x.length);   // ERREUR : 'length' n'existe pas sur 'T'
}
interface AvecLongueur { length: number; }

function afficherLongueur<T extends AvecLongueur>(x: T): void {
  console.log(x.length);   // OK
}

afficherLongueur("bonjour");   // 7
afficherLongueur(42);          // ERREUR : number n'a pas de length

Plus la contrainte est forte, moins le generic est réutilisable, mais plus vous pouvez en faire à l’intérieur.

Exceptions

Pourquoi des exceptions

En C, une fonction qui échoue renvoie un code d’erreur. L’appelant oublie de le tester, et l’erreur se propage silencieusement.

// Style C : le chemin nominal est noyé dans les tests
const a = lire();
if (a === null) return -1;
const b = convertir(a);
if (b === null) return -2;

// Style exception : le chemin nominal est visible d'un coup d'œil
try {
  const b = convertir(lire());
} catch (e) { /* tout le traitement d'erreur est ici */ }

Le bénéfice principal est la séparation des deux codes.

throw

class Calculatrice {
  diviser(a: number, b: number): number {
    if (b === 0) {
      throw new Error("Division par zéro n'est pas autorisée");
    }
    return a / b;
  }
}

throw interrompt immédiatement la fonction et remonte la pile d’appels jusqu’à trouver un try. Sans try, le programme s’arrête.

JavaScript permet throw "oups" ou throw 42. Ne le faites jamais : vous perdez le message structuré, le nom et surtout la pile d’appels.

try / catch / finally

Bloc Rôle Obligatoire
try le code susceptible d’échouer oui
catch reçoit l’erreur et la traite l’un des deux
finally s’exécute dans tous les cas l’un des deux

finally s’exécute que le try réussisse, échoue, ou même exécute un return. C’est là qu’on libère les ressources.

Contrairement à Java ou C++, il n’y a qu’un seul catch. On distingue les cas soi-même.

catch reçoit un unknown

try {
  risque();
} catch (e) {
  console.error(e.message);   // ERREUR : 'e' is of type 'unknown'
}

Puisque n’importe quoi peut être levé, TypeScript refuse de supposer.

try {
  risque();
} catch (e: unknown) {
  if (e instanceof Error) {
    console.error(e.message);   // ici TypeScript sait
  } else {
    console.error("Erreur non standard :", e);
  }
}

catch (e: any) compile aussi, et replante le jour où vous cherchez le bug.

Ses propres classes d’erreur

Comparer des chaînes de messages est fragile. On crée une classe par type d’erreur.

class ErreurDivisionParZero extends Error {
  constructor() {
    super("Division par zéro n'est pas autorisée");
    this.name = "ErreurDivisionParZero";
  }
}
class ErreurValeurTropGrande extends Error {
  constructor(public valeur: number) {
    super(`La valeur ${valeur} dépasse 100`);
    this.name = "ErreurValeurTropGrande";
  }
}

Tout l’intérêt : une erreur peut porter des données, pas seulement un texte.

Distinguer avec instanceof

try {
  console.log(this.diviser(a, b));
} catch (e: unknown) {
  if (e instanceof ErreurDivisionParZero) {
    console.error("Division par zéro !");
  } else if (e instanceof ErreurValeurTropGrande) {
    console.error(`${e.valeur} est trop grand`);   // accès à la donnée
  } else if (e instanceof Error) {
    console.error("Erreur inattendue :", e.message);
  }
}

Après le test, TypeScript réduit le type de e et donne accès aux propriétés spécifiques. Ordonnez du plus précis au plus général.

À vous : par où commencer

Parcours essentiel, dans l’ordre

  • 5.1 Aller-retour
  • 5.2 Valider avant d’utiliser
  • 5.4 Du any au generic
  • 5.7 Lire l’ordre d’exécution
  • 5.8 Rectangle

Ensuite

  • 5.3 Données contre comportement (entraînement, transfert)
  • 5.5 Une file générique (entraînement, transfert)
  • 5.9 Erreurs personnalisées (entraînement, transfert)
  • 5.6 Contraintes (approfondissement)
  • 5.10 Exception ou valeur de retour ? (approfondissement)