Évaluation 2 — Corrigé

2025-2026 · tout le cours

Le sujet correspondant : Évaluation 2 — Sujet.

ImportantNe lisez pas ce corrigé trop tôt

Chaque réponse est repliée. Traitez d’abord la question, puis dépliez pour comparer. Un corrigé lu avant l’effort ne vous apprend presque rien.

Partie 1 — Questions théoriques

Barème : 10 points

a)

JSON.stringify(monChat) retourne la chaîne :

{"nom":"Felix","age":3}

Seules les propriétés de données (nom et age) sont présentes. Les méthodes (crier() et ronronner()) n’y figurent pas. C’est parce que JSON est un format de transport de données uniquement. JSON.stringify() ignore toutes les propriétés dont la valeur est une fonction.

b)

À l’exécution, chatRestaure.crier() provoque une erreur : chatRestaure.crier is not a function.

TypeScript ne détecte pas ce problème car JSON.parse() retourne any. En écrivant const chatRestaure: IAnimal = JSON.parse(json), on affirme à TypeScript que le résultat est de type IAnimal, mais aucune vérification n’est faite à l’exécution. L’objet retourné est un simple {nom: "Felix", age: 3} sans aucune méthode.

Dans un projet réel recevant des données d’une API via fetch, c’est particulièrement dangereux car :

  • On fait confiance au serveur pour envoyer des données conformes à l’interface
  • Si l’API change sa structure ou renvoie des données inattendues, TypeScript ne peut pas le détecter
  • On peut appeler des méthodes qui n’existent pas sur l’objet désérialisé, causant des crashs à l’exécution chez l’utilisateur

c)

function toChat(data: IAnimal): Chat {
    return new Chat(data.nom, data.age);
}

const chatRestaure: IAnimal = JSON.parse(json);
const vraiChat: Chat = toChat(chatRestaure);
console.log(vraiChat.crier());      // "Miaou !" ✓
console.log(vraiChat.ronronner());   // "Rrrr..." ✓

En créant une nouvelle instance de Chat via le constructeur, on obtient un véritable objet avec ses méthodes (héritées du prototype). toChat copie les données de l’objet désérialisé dans une vraie instance.

a)

boiteNombres.ajouter("test") produit une erreur de compilation :

Argument of type ‘string’ is not assignable to parameter of type ‘number’.

boiteNombres est de type Boite<number>, donc T = number. La méthode ajouter attend un number. Les génériques assurent la sécurité en remplaçant T par le type concret à chaque utilisation.

Le type de valeur est number | undefined car retirer() retourne T | undefined. Le undefined est inclus car si la boîte est vide, pop() retourne undefined.

b)

boite.ajouter("hello");
const n: number = boite.retirer();  // "hello" à l'exécution !
console.log(n.toFixed(2));          // CRASH : n.toFixed is not a function

Les génériques empêchent ce scénario car le compilateur refuserait ajouter("hello") sur une Boite<number>, détectant le mélange de types avant l’exécution.

c)

Oui, const boiteMixte = new Boite<number | string>() permet d’ajouter les deux types :

boiteMixte.ajouter(42);       // OK
boiteMixte.ajouter("hello");  // OK

Le type de retour de retirer() est number | string | undefined. Avant utilisation, il faudrait vérifier le type avec typeof :

const val = boiteMixte.retirer();
if (typeof val === "number") {
    console.log(val.toFixed(2));  // OK, TypeScript sait que c'est un number
}

a)

Les lignes affichées sont :

A: Alice
Erreur nom: Nom invalide : "B"
Fin
  1. creerUtilisateur("Alice", 25) réussit → affiche "A: Alice"
  2. creerUtilisateur("B", 30) → "B" a 1 caractère (< 2) → throw new ErreurNom("B")
  3. L’exception interrompt immédiatement le bloc try. u3 n’est jamais atteint.
  4. Le catch attrape l’erreur : e instanceof ErreurNom → affiche l’erreur
  5. Le finally s’exécute toujours → affiche "Fin"

La création de u3 n’est jamais atteinte car après un throw, l’exécution saute directement au catch. Un seul throw par bloc try.

b)

L’ordre est important car ErreurNom et ErreurAge héritent de Error. Donc new ErreurNom(...) est aussi une instance de Error.

Si on testait e instanceof Error en premier, ce test serait toujours vrai pour toutes les erreurs (y compris ErreurNom et ErreurAge), et on entrerait toujours dans ce premier if sans jamais atteindre les cas spécifiques. Il faut tester les classes les plus spécifiques d’abord, la plus générale en dernier.

c)

Avec des try/catch séparés pour chaque appel, l’avantage est que les trois créations sont tentées même si l’une échoue. Le code continue après chaque catch. On peut ainsi détecter et signaler toutes les erreurs, pas seulement la première.

Avec un try unique, dès qu’un appel échoue, les suivants sont ignorés (comme on l’a vu avec u3).

a)

Ordre d’affichage :

A. Avant
1. Début
B. Après
2. Terminé après 1000ms
4. Durée négative
5. Fin

"B. Après" s’affiche avant "2." car scenario() est async. Quand elle atteint await attendre(1000), elle suspend son exécution et rend la main à l’appelant. Le code synchrone après scenario() continue donc immédiatement.

C’est la boucle d’événements (event loop) : scenario() retourne une promesse non résolue, et le code synchrone suivant s’exécute pendant l’attente.

b)

  • attendre(1000) retourne Promise<string>
  • resultat après await est de type string

await suspend la fonction async et déballe (unwrap) la valeur résolue de la promesse. Il transforme Promise<T> en T. Si la promesse est rejetée, l’erreur est levée comme une exception.

c)

Si on utilise await scenario(), le code attend la fin complète de scenario() avant de continuer. L’ordre devient :

A. Avant
1. Début
2. Terminé après 1000ms
4. Durée négative
5. Fin
B. Après

"B. Après" s’affiche maintenant après "5. Fin" car await bloque l’exécution de la fonction englobante jusqu’à ce que scenario() soit entièrement terminée.

d)

attendre(1000).then((resultat) => {
    console.log("2.", resultat);
});

Partie A

a)

Le fetch dans le frontmatter s’exécute côté serveur (sur le serveur Astro). Les restrictions CORS ne s’appliquent pas car c’est une politique de sécurité des navigateurs, pas des serveurs.

Le fetch dans le <script> s’exécute côté client (dans le navigateur). Le navigateur applique la politique de même origine et vérifie si le serveur distant autorise les requêtes depuis le domaine courant via les en-têtes CORS.

b)

Solution 1 — Technique data-* : faire le fetch côté serveur dans le frontmatter, puis injecter les données dans un attribut data-payload via JSON.stringify(). Côté client, relire avec dataset puis JSON.parse().

Solution 2 — Le serveur api.exemple.com ajoute l’en-tête Access-Control-Allow-Origin: * à ses réponses, autorisant les requêtes depuis n’importe quel domaine.

Partie B

c)

Le problème est l’absence de event.preventDefault(). Sans cette ligne, le formulaire effectue son comportement par défaut : envoyer les données et recharger la page. L’alerte est mise à jour brièvement, puis la page se recharge et tout revient à l’état initial.

Correction — ajouter comme première ligne du gestionnaire :

event.preventDefault();

d)

Avec function(event) { ... }, this fait référence à l’élément qui a déclenché l’événement (ici le <form>), car c’est le comportement par défaut de addEventListener.

Si on remplace par une fonction fléchée (event) => { ... }, this fait référence au contexte lexical englobant (généralement window ou le scope du module). Les fonctions fléchées n’ont pas leur propre this — elles capturent celui du contexte dans lequel elles sont définies.


Partie 2 — Exercice de codage

Barème : 10 points

import { Piste, pistesJSON, pistesJSON_invalide, formatDuree } from "./catalogueMusique.ts";

const parsePistes = (json: string): Piste[] => {
    try {
        const resultat: Piste[] = JSON.parse(json);
        return resultat;
    } catch (e: unknown) {
        if (e instanceof Error) {
            console.error("Erreur de parsing JSON :", e.message);
        }
        return [];
    }
};

// Test avec le JSON invalide
const pistesInvalides = parsePistes(pistesJSON_invalide);
console.log("Pistes invalides (devrait être vide) :", pistesInvalides);

// Parse le JSON valide
const pistes: Piste[] = parsePistes(pistesJSON);

const creerLigneTableau = (piste: Piste): string => {
    const classe = piste.note >= 4.7 ? "top" : "normal";
    return `<tr class="${classe}">
        <td>${piste.titre}</td>
        <td>${piste.artiste}</td>
        <td>${piste.album}</td>
        <td>${formatDuree(piste.duree)}</td>
        <td>${piste.genre}</td>
        <td>${piste.note}</td>
    </tr>`;
};

const catalogueDiv = document.getElementById("catalogue") as HTMLDivElement;
const lignes = pistes.map(creerLigneTableau);
catalogueDiv.innerHTML = `<table>
    <tr><th>Titre</th><th>Artiste</th><th>Album</th><th>Durée</th><th>Genre</th><th>Note</th></tr>
    ${lignes.join("")}
</table>`;

Résultats attendus :

  • parsePistes(pistesJSON_invalide) retourne [] et affiche une erreur SyntaxError dans la console (le JSON fourni est volontairement mal formé).
  • parsePistes(pistesJSON) retourne les 10 pistes.
  • 5 pistes reçoivent la classe top (note ≥ 4,7) : Bohemian Rhapsody (5), Billie Jean (4.8), Smells Like Teen Spirit (4.7), Hey Jude (4.9), Purple Rain (4.7). Les 5 autres ont la classe normal.

Barème détaillé :

  • Import correct des 4 éléments (interface, 2 chaînes JSON, fonction) : 0,50 pt
  • Fonction fléchée parsePistes avec try/catch, retour Piste[] et [] en cas d’erreur : 1,00 pt
  • Test explicite avec pistesJSON_invalide : 0,50 pt
  • Fonction fléchée creerLigneTableau avec paramètre typé et retour HTML : 0,50 pt
  • Logique conditionnelle pour la classe CSS top/normal : 0,50 pt
  • Utilisation de map + .join("") + affichage dans le DOM : 0,50 pt
class PisteCard extends HTMLElement {
    static get observedAttributes(): string[] {
        return ["titre", "artiste"];
    }

    connectedCallback() {
        this.render();
    }

    attributeChangedCallback() {
        this.render();
    }

    render() {
        const titre = this.getAttribute("titre") || "Sans titre";
        const artiste = this.getAttribute("artiste") || "Artiste inconnu";
        this.innerHTML = `<strong>${titre}</strong> — <em>${artiste}</em>`;
    }
}

customElements.define("piste-card", PisteCard);

const composantsDiv = document.getElementById("composants") as HTMLDivElement;
pistes.slice(0, 5).forEach((piste) => {
    const card = document.createElement("piste-card");
    card.setAttribute("titre", piste.titre);
    card.setAttribute("artiste", piste.artiste);
    composantsDiv.appendChild(card);
});

Résultats attendus : 5 cartes sont créées, dans l’ordre du catalogue :

  1. Bohemian Rhapsody — Queen
  2. Imagine — John Lennon
  3. Billie Jean — Michael Jackson
  4. Smells Like Teen Spirit — Nirvana
  5. Like a Rolling Stone — Bob Dylan
Avertissement

Le nom du custom element doit obligatoirement contenir un tiret (piste-card), sinon customElements.define lève une erreur.

Barème détaillé :

  • Classe héritant de HTMLElement + observedAttributes retournant ["titre", "artiste"] : 0,75 pt
  • connectedCallback() et attributeChangedCallback() appelant render() : 0,75 pt
  • render() utilisant getAttribute et produisant <strong>…</strong> — <em>…</em> : 0,50 pt
  • customElements.define("piste-card", PisteCard) : 0,50 pt
  • slice(0, 5) + forEach + createElement / setAttribute / appendChild : 0,50 pt
const searchForm = document.getElementById("searchForm") as HTMLFormElement;
const searchInput = document.getElementById("searchInput") as HTMLInputElement;
const rechercheDiv = document.getElementById("recherche") as HTMLDivElement;

searchForm.addEventListener("submit", (event: Event) => {
    event.preventDefault();

    const recherche = searchInput.value.toLowerCase();
    const resultats = pistes.filter((piste) =>
        piste.artiste.toLowerCase().includes(recherche)
    );

    if (resultats.length === 0) {
        rechercheDiv.innerHTML = `<p class="erreur">Aucune piste trouvée pour cet artiste</p>`;
    } else {
        const items = resultats.map((piste) =>
            `<li>${piste.titre} — ${piste.artiste} (${piste.album}, ${formatDuree(piste.duree)})</li>`
        );
        rechercheDiv.innerHTML = `<ul>${items.join("")}</ul>`;
    }
});

// Durée totale avec reduce
const statsDiv = document.getElementById("statistiques") as HTMLDivElement;
const dureeTotale = pistes.reduce((acc, piste) => acc + piste.duree, 0);
statsDiv.innerHTML = `<p><strong>Durée totale de la playlist :</strong> ${formatDuree(dureeTotale)}</p>`;

Résultats attendus :

  • Recherche « queen » → 1 résultat : Bohemian Rhapsody — Queen (A Night at the Opera, 5:54)
  • Recherche « bob » → 2 résultats : Like a Rolling Stone — Bob Dylan (Highway 61 Revisited, 6:09) et Redemption Song — Bob Marley (Uprising, 3:47)
  • Recherche « zzz » → Aucune piste trouvée pour cet artiste (classe erreur)
  • Durée totale : 3285 secondes → 54:45
Avertissement

Sans event.preventDefault(), le formulaire recharge la page et les résultats disparaissent immédiatement.

Barème détaillé :

  • Récupération du formulaire et de l’input + gestionnaire submit : 0,50 pt
  • event.preventDefault() : 0,50 pt
  • filter insensible à la casse avec .toLowerCase() + .includes() : 1,00 pt
  • Gestion du cas « aucun résultat » avec la classe erreur : 0,50 pt
  • Affichage des résultats en liste <ul><li> au bon format : 0,50 pt
  • Durée totale calculée avec reduce et formatée avec formatDuree : 0,50 pt