Rattrapage — Corrigé
2025-2026 · tout le cours
Le sujet correspondant : Rattrapage — Sujet.
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 indicatif : environ 2 points par question. À ajuster après lecture des copies.
a)
| Ligne | Verdict | Justification |
|---|---|---|
compteur = "5"; |
Refusée | compteur a été initialisé avec 0 : TypeScript a inféré le type number. On ne peut pas y affecter une chaîne. |
const total1 = libre * 2; |
Acceptée | libre est de type any : le compilateur désactive toute vérification sur cette variable. |
const total2 = donnee * 2; |
Refusée | donnee est de type unknown : TypeScript interdit toute opération tant que le type n’a pas été vérifié. |
Point important pour total1 : le code compile mais rien ne garantit le résultat. À l’exécution, JavaScript convertit "42" en nombre et total1 vaut 84. Avec une autre chaîne ("abc"), on obtiendrait NaN sans aucun avertissement.
b)
unknown est le type sûr. Les deux acceptent n’importe quelle valeur en entrée, mais :
- avec
any, on peut tout faire avec la valeur : le compilateur ne vérifie plus rien, les erreurs n’apparaissent qu’à l’exécution ; - avec
unknown, on ne peut rien faire tant qu’on n’a pas prouvé le type. Le compilateur force la vérification.
Pour utiliser une variable unknown, on réduit son type (narrowing), par exemple avec typeof :
if (typeof donnee === "string") {
const total = Number(donnee) * 2; // ici TypeScript sait que donnee est une string
}On peut aussi utiliser instanceof pour les objets, ou une assertion as (moins sûr, car non vérifiée à l’exécution).
c)
innerHTML interprète la chaîne reçue comme du HTML et non comme du texte. Le navigateur construit donc une vraie balise <img>, tente de charger l’image x, échoue, ce qui déclenche l’attribut onerror : le JavaScript qu’il contient est exécuté.
C’est une faille XSS (Cross-Site Scripting) : du code fourni par l’utilisateur s’exécute dans la page, avec les mêmes droits que le code légitime. Un attaquant peut ainsi voler des cookies de session, modifier la page ou envoyer des requêtes à la place de la victime.
Correction — remplacer innerHTML par textContent :
zone.textContent = champ.value;textContent insère la valeur comme du texte brut : les caractères < et > sont affichés tels quels et le navigateur ne construit aucune balise. Il n’y a donc aucun code à exécuter.
a)
Ce n’est pas une erreur. Exporter rend un élément disponible pour d’autres fichiers, sans obliger personne à l’importer. C’est courant dans un module utilitaire qui expose plusieurs fonctions dont chaque fichier ne consomme que ce dont il a besoin. Le compilateur ne signale rien.
Le cas inverse est en revanche une erreur de compilation : importer un élément non exporté échoue avec un message du type « Module “./geometrie.ts” has no exported member ‘perimetreCercle’ ». Sans export, l’élément reste privé au module.
b)
Étape par étape :
mapappliqueaireCercleà chaque rayon :[3.14159, 12.56636, 28.27431, 50.26544, 78.53975]filterne garde que les valeurs strictement supérieures à 20 :[28.27431, 50.26544, 78.53975]reduceadditionne ces trois valeurs à partir de 0 :157.0795
L’affichage final est donc 157.08 (arrondi à 2 décimales par toFixed(2)).
Le 0 est la valeur initiale de l’accumulateur. Il fixe le point de départ de la somme et, surtout, il garantit un résultat correct même si le tableau filtré était vide (on obtiendrait 0 au lieu d’une erreur).
c)
grand vaut undefined : aucun rayon du tableau n’est supérieur à 10.
Son type est number | undefined. TypeScript impose ce type parce que find peut ne rien trouver : c’est une possibilité connue à la compilation, donc elle est inscrite dans le type. C’est une bonne chose car le compilateur oblige à traiter explicitement le cas « pas de résultat », au lieu de le découvrir par un plantage en production.
grand.toFixed(2) est refusé à la compilation (« ‘grand’ is possibly ‘undefined’ »). Il faut d’abord tester :
if (grand !== undefined) {
console.log(grand.toFixed(2));
}a)
Affichage :
Zoe : 312 km
VanMoof : 40 km
2
Détail : 52 × 6 = 312 pour la voiture, 0.5 × 80 = 40 pour le vélo. Le compteur statique vaut 2 car le constructeur de Vehicule a été appelé deux fois (via super(nom)).
const v = new Vehicule("Test"); provoque une erreur de compilation : « Cannot create an instance of an abstract class ». Une classe abstraite sert uniquement de modèle commun ; elle est incomplète (ici getAutonomie() n’a pas d’implémentation) et ne peut donc pas être instanciée directement.
b)
C’est le polymorphisme (on parle aussi de liaison dynamique ou dynamic dispatch).
Le type déclaré IVehicule ne suffit pas à choisir la méthode : le choix se fait à l’exécution, en fonction de la classe réelle de l’objet pointé par v. Chaque objet garde le lien vers sa propre classe, et c’est la version de getAutonomie() définie dans cette classe qui est appelée. C’est ce qui permet de traiter uniformément une collection d’objets hétérogènes.
c)
Une classe ne peut hériter que d’une seule classe parente, mais elle peut implémenter autant d’interfaces qu’on veut. Une classe venue d’une bibliothèque extérieure ne peut pas être modifiée pour ajouter extends Vehicule, et elle hérite peut-être déjà d’une autre classe : l’héritage est donc bloqué.
Avec l’interface, l’intégration reste possible. Il suffit d’écrire une petite classe d’adaptation qui implémente IVehicule et délègue le travail à l’objet extérieur :
class TrottinetteAdaptee implements IVehicule {
private trottinette: TrottinetteExterne;
constructor(trottinette: TrottinetteExterne) {
this.trottinette = trottinette;
}
getNom(): string { return this.trottinette.label; }
getAutonomie(): number { return this.trottinette.rangeKm; }
}Cet objet peut alors rejoindre le tableau flotte sans rien changer au code existant. Typer par l’interface décrit ce que l’objet sait faire plutôt que d’où il vient : c’est ce qui rend le code extensible.
a)
JSON.stringify(p) produit :
{"nom":"Clavier","prix":45}Seules les données sont sérialisées. La méthode afficher() n’y figure pas : JSON ne transporte pas de fonctions.
restaure.afficher() compile sans erreur (l’interface IProduit déclare bien cette méthode, et on a annoncé au compilateur que le résultat du parsing était un IProduit), mais échoue à l’exécution avec restaure.afficher is not a function.
La cause : JSON.parse retourne un objet JavaScript ordinaire, porteur des seules propriétés nom et prix. Il n’est pas relié à la classe Produit, donc il n’a accès à aucune de ses méthodes.
b)
Il faut reconstruire de vraies instances après le parsing :
function toProduit(data: IProduit): Produit {
return new Produit(data.nom, data.prix);
}
const brut: IProduit = JSON.parse(json);
const produit: Produit = toProduit(brut);
console.log(produit.afficher()); // fonctionneDans un cas réel, on ajoute aussi une vérification des données reçues avant la conversion, car rien ne garantit que le serveur envoie ce qu’on attend.
Le typage statique ne protège pas ici pour deux raisons :
JSON.parseretourneany: l’annotation: IProduitest une promesse faite au compilateur, pas une vérification ;- TypeScript disparaît à la transpilation. Il ne reste aucun contrôle de type à l’exécution, donc rien ne peut détecter que les données arrivées ne correspondent pas au type annoncé.
c)
retirer(): T {
const article = this.articles.pop();
if (article === undefined) {
throw new ErreurStockVide();
}
return article;
}Le type de retour devient T (et non plus T | undefined) : l’absence d’article est désormais signalée par une exception.
try {
const article = panier.retirer();
console.log(article);
} catch (e: unknown) {
if (e instanceof ErreurStockVide) {
console.error("Panier vide, rien à retirer.");
} else if (e instanceof Error) {
console.error("Erreur inattendue :", e.message);
}
}L’ordre compte parce que ErreurStockVide hérite de Error : toute ErreurStockVide est aussi une Error. Si on testait e instanceof Error en premier, ce test serait vrai dans tous les cas et le programme n’entrerait jamais dans la branche spécifique. On teste donc du plus spécifique au plus général.
À noter également : e est typé unknown car on peut lancer n’importe quelle valeur en JavaScript, pas seulement des Error. Le instanceof est donc obligatoire avant d’accéder à e.message.
a)
Ordre réel :
1. Début
2. Chargement...
3. Fin
4. Profil 7
afficher() est une fonction async. Elle s’exécute normalement jusqu’au await : c’est pourquoi "2. Chargement..." apparaît tout de suite. Au moment du await, la fonction se met en pause et rend la main à l’appelant, sans attendre la fin de la promesse. Le programme principal reprend donc immédiatement et affiche "3. Fin". Ce n’est qu’une fois la promesse résolue (500 ms plus tard) que la suite de afficher() reprend et affiche "4. Profil 7".
Autrement dit, await suspend la fonction asynchrone, pas le programme entier : c’est ce qui permet à l’interface de rester réactive pendant une opération longue.
Gestion du rejet :
async function afficher() {
console.log("2. Chargement...");
try {
const profil = await chargerProfil(7);
console.log("4.", profil);
} catch (e: unknown) {
if (e instanceof Error) {
console.error("Erreur :", e.message);
}
}
}Quand une promesse attendue avec await est rejetée, l’erreur est levée comme une exception classique : elle se rattrape avec try/catch.
b)
Le frontmatter Astro (entre les ---) s’exécute sur le serveur, au moment de la génération de la page. CORS est une règle appliquée par les navigateurs, pas par les serveurs : un programme serveur peut donc appeler n’importe quelle API sans restriction.
Le <script> s’exécute dans le navigateur. Celui-ci applique la politique de même origine : il vérifie que la réponse de l’API extérieure contient bien un en-tête l’autorisant (Access-Control-Allow-Origin). Si cet en-tête est absent, le navigateur bloque l’accès au résultat.
Technique vue en cours — faire l’appel côté serveur puis transmettre les données à la page :
- côté serveur,
fetchpuis préparation des données utiles ; - sérialisation dans un attribut
data-*:<div id="donnees" data-payload={JSON.stringify(films)}></div>; - côté client, relecture via
datasetpuisJSON.parse().
Le navigateur ne contacte jamais l’API extérieure : il lit seulement le HTML produit par le serveur. Le problème CORS disparaît, et on évite une deuxième requête réseau.
c)
Il manque event.preventDefault(), à placer en première instruction du gestionnaire :
form.addEventListener("submit", (event: Event) => {
event.preventDefault();
badge.setAttribute("pseudo", input.value);
});Sans cet appel, le navigateur exécute le comportement par défaut d’un formulaire : il l’envoie et recharge la page. L’attribut est bien modifié (d’où le très bref affichage correct), mais le rechargement réinitialise aussitôt le DOM et tout le JavaScript.
observedAttributes est un accesseur statique qui indique au navigateur quels attributs surveiller. Seuls ceux qui y figurent déclenchent attributeChangedCallback.
Si on retirait "pseudo" de cette liste, attributeChangedCallback ne serait plus jamais appelé pour cet attribut. Le composant s’afficherait une seule fois, lors de son insertion dans le DOM (connectedCallback), et les setAttribute("pseudo", ...) suivants modifieraient bien l’attribut dans le DOM mais sans provoquer de nouveau rendu : le badge resterait figé.
Partie 2 — Exercice de codage
Barème indicatif : environ 3 points pour S1, 3 pour S2, 4 pour S3. À ajuster après lecture des copies.
Valeurs de référence
Ces valeurs permettent de vérifier rapidement une copie :
| Élément | Valeur attendue |
|---|---|
| Nombre de films | 10 |
| Durée totale | 1434 minutes → 23h54 |
| Note moyenne | 8.54 |
| Films « récents » (≥ 2010) | Inception (2010), Interstellar (2014), Dunkerque (2017), Django Unchained (2012) → 4 films |
| Recherche « nolan » | Inception, Interstellar, Dunkerque → 3 films |
| Recherche « tarantino » | Pulp Fiction, Kill Bill, Django Unchained → 3 films |
| Recherche « xyz » | « Aucun film trouvé » |
parseFilms(filmsJSON_invalide) |
[] + message d’erreur en console, sans planter la page |
import { Film, filmsJSON, filmsJSON_invalide, formatDuree } from "./catalogueFilms.ts";
const parseFilms = (json: string): Film[] => {
try {
const resultat: Film[] = JSON.parse(json);
return resultat;
} catch (e: unknown) {
if (e instanceof Error) {
console.error("Erreur de parsing JSON :", e.message);
}
return [];
}
};
const filmsInvalides = parseFilms(filmsJSON_invalide);
console.log("Films invalides (tableau vide attendu) :", filmsInvalides);
const films: Film[] = parseFilms(filmsJSON);
const creerLigne = (film: Film): string =>
`<tr>
<td>${film.titre}</td>
<td>${film.realisateur}</td>
<td>${film.annee}</td>
<td>${formatDuree(film.duree)}</td>
<td>${film.genre}</td>
<td>${film.note}</td>
</tr>`;
const catalogueDiv = document.getElementById("catalogue") as HTMLDivElement;
const lignes: string[] = films.map(creerLigne);
catalogueDiv.innerHTML = `<table>
<tr><th>Titre</th><th>Réalisateur</th><th>Année</th><th>Durée</th><th>Genre</th><th>Note</th></tr>
${lignes.join("")}
</table>`;Points de contrôle
- L’import utilise bien le module (
from "./catalogueFilms.ts"). parseFilmsest une fonction fléchée et contient untry/catch.- Le
catchretourne un tableau vide : la page continue de fonctionner malgré le JSON invalide. creerLigneretourne une chaîne<tr>avec les 6 colonnes dans l’ordre demandé.- La durée passe par
formatDuree(et non affichée en minutes brutes). mapest utilisé (pas une bouclefor), suivi dejoin("").- Le tableau HTML comporte une ligne d’en-têtes.
Erreurs fréquentes : oublier join("") (le tableau s’affiche avec des virgules), placer le try/catch autour de l’affichage plutôt qu’autour du JSON.parse, ou ne pas tester le JSON invalide.
class FilmDetaille implements Film {
titre: string;
realisateur: string;
annee: number;
duree: number;
genre: string;
note: number;
constructor(film: Film) {
this.titre = film.titre;
this.realisateur = film.realisateur;
this.annee = film.annee;
this.duree = film.duree;
this.genre = film.genre;
this.note = film.note;
}
estRecent(): boolean {
return this.annee >= 2010;
}
getResume(): string {
return `${this.titre} (${this.annee}) de ${this.realisateur} — ${this.note}/10`;
}
}
class Collection<T> {
private elements: T[] = [];
ajouter(element: T): void {
this.elements.push(element);
}
getTous(): T[] {
return this.elements;
}
taille(): number {
return this.elements.length;
}
}
const collection = new Collection<FilmDetaille>();
films.forEach((film) => collection.ajouter(new FilmDetaille(film)));
const fichesDiv = document.getElementById("fiches") as HTMLDivElement;
const items: string[] = collection.getTous().map((film) => {
const marque = film.estRecent() ? " [récent]" : "";
return `<li>${film.getResume()}${marque}</li>`;
});
fichesDiv.innerHTML = `<ul>${items.join("")}</ul>
<p>Total : ${collection.taille()} films dans la collection.</p>`;Points de contrôle
FilmDetaille implements Filmdéclare bien les six propriétés.- Le constructeur prend un objet
Filmet recopie les propriétés. estRecent()retourne unbooleanet compare à2010inclus (>=).getResume()respecte le format"Titre (année) de Réalisateur — note/10".Collection<T>est réellement générique : le paramètreTest utilisé pourelements,ajouteretgetTous.elementsestprivate.- La collection est instanciée en
Collection<FilmDetaille>. - La mention
[récent]n’apparaît que pour les films concernés (4 attendus). - Le total final utilise
taille().
Erreurs fréquentes : déclarer Collection sans généricité (typée directement FilmDetaille[]), oublier d’instancier FilmDetaille et ajouter les objets bruts du JSON (les méthodes n’existent alors pas), ou utiliser > au lieu de >= pour 2010.
class FilmCard extends HTMLElement {
static get observedAttributes(): string[] {
return ["titre", "realisateur"];
}
connectedCallback() {
this.render();
}
attributeChangedCallback() {
this.render();
}
render() {
const titre = this.getAttribute("titre") || "Sans titre";
const realisateur = this.getAttribute("realisateur") || "Inconnu";
this.innerHTML = `<strong>${titre}</strong> — <em>${realisateur}</em>`;
}
}
customElements.define("film-card", FilmCard);
const rechercheForm = document.getElementById("rechercheForm") as HTMLFormElement;
const rechercheInput = document.getElementById("rechercheInput") as HTMLInputElement;
const resultatsDiv = document.getElementById("resultatsRecherche") as HTMLDivElement;
rechercheForm.addEventListener("submit", (event: Event) => {
event.preventDefault();
const recherche = rechercheInput.value.toLowerCase();
const trouves = films.filter((film) =>
film.realisateur.toLowerCase().includes(recherche)
);
resultatsDiv.innerHTML = "";
if (trouves.length === 0) {
resultatsDiv.textContent = "Aucun film trouvé";
} else {
trouves.forEach((film) => {
const carte = document.createElement("film-card");
carte.setAttribute("titre", film.titre);
carte.setAttribute("realisateur", film.realisateur);
resultatsDiv.appendChild(carte);
});
}
});
const statsDiv = document.getElementById("statistiques") as HTMLDivElement;
function calculerStatistiques(): Promise<string> {
return new Promise<string>((resolve) => {
setTimeout(() => {
const dureeTotale = films.reduce((total, film) => total + film.duree, 0);
const noteMoyenne = films.reduce((total, film) => total + film.note, 0) / films.length;
resolve(
`Durée totale : ${formatDuree(dureeTotale)} — Note moyenne : ${noteMoyenne.toFixed(2)}/10`
);
}, 1000);
});
}
async function afficherStatistiques() {
statsDiv.textContent = "Calcul en cours...";
const resultat = await calculerStatistiques();
statsDiv.textContent = resultat;
}
afficherStatistiques();Points de contrôle
observedAttributesretourne bien["titre", "realisateur"].connectedCallbacketattributeChangedCallbackappellentrender().customElements.define("film-card", FilmCard)est présent, avec un nom contenant un tiret.event.preventDefault()est présent dans le gestionnairesubmit.- La recherche est insensible à la casse (
toLowerCase()des deux côtés). - Le conteneur de résultats est vidé avant chaque nouvelle recherche.
- Les cartes sont créées avec
createElement+setAttributepuisappendChild. - Le cas « aucun résultat » est traité.
calculerStatistiquesretourne unePromise<string>résolue viasetTimeout(1 s).reduceest utilisé pour la durée totale et pour la note moyenne.- Le message
"Calcul en cours..."est affiché avant leawait.
Erreurs fréquentes : oublier preventDefault (la page se recharge), ne pas vider le conteneur (les résultats s’accumulent), appeler resolve en dehors du setTimeout (le délai n’est plus respecté), ou oublier le await (affichage d’un objet Promise).
Remarques de correction
- Une section partiellement écrite doit être valorisée : les concepts sont indépendants les uns des autres.
- Le rendu visuel exact n’est pas noté ; ce sont les concepts (généricité, promesse, cycle de vie du composant, gestion d’erreur) qui comptent.
- Si un étudiant remplace
innerHTMLpartextContentlà où du HTML est attendu, signaler mais ne pas trop pénaliser : le réflexe de sécurité est bon en soi.