Rattrapage — Sujet
2025-2026 · tout le cours
Informations générales
- Durée : 3 heures
- Note : /20 — cet examen remplace la moyenne des deux examens précédents
- Documents autorisés : le site web du cours uniquement — https://remisharrock.gitlab.io/typescript101/
- Internet : non disponible (mode examen hors ligne)
- Intelligence artificielle : interdite
- Programme : l’ensemble du cours, séances 1 à 7
| Partie | Contenu | Points |
|---|---|---|
| Partie 1 | 5 questions théoriques (environ 2 points chacune) | 10 points |
| Partie 2 | 3 sections de codage (environ 3 à 4 points chacune) | 10 points |
| Total | 20 points |
Le barème définitif est ajusté après correction. Les points indiqués sont donnés à titre indicatif pour vous aider à gérer votre temps.
Mise en place (5 minutes)
Téléchargez l’archive outillage.tar.gz depuis la page Outillage et décompressez-la.
- Copiez le dossier
outillage/projet_templateen entier, renommez la copiemon_projet(à la souris, ou aveccp -R outillage/projet_template mon_projet—-Ren majuscule). - Dans un terminal :
cd mon_projetpuisnpx astro dev - Ouvrez
http://localhost:4321/dans le navigateur. - Vos fichiers
.astrovont danssrc/pages/, vos fichiers.tsdanssrc/scripts/.
Le détail complet et la résolution de problèmes se trouvent dans outillage/README.html.
Partie 1 — Questions théoriques
Durée indicative : 1h15 — environ 2 points par question
Répondez dans le fichier reponse_NOM.txt, entre les balises %%%DEBUT_Q1%%% … %%%FIN_Q1%%%, etc.
Les réponses attendues sont courtes : quelques phrases précises valent mieux qu’un long paragraphe. Quand une justification est demandée, c’est le raisonnement qui est noté, pas la longueur.
Question 1 — Typage, inférence et manipulation du DOM
let compteur = 0;
const seuil: number = 10;
let donnee: unknown = "42";
let libre: any = "42";
compteur = "5";
const total1 = libre * 2;
const total2 = donnee * 2;
const champ = document.getElementById("saisie") as HTMLInputElement;
const zone = document.getElementById("sortie") as HTMLDivElement;
champ.addEventListener("input", () => {
zone.innerHTML = champ.value;
});a) Parmi les lignes compteur = "5";, const total1 = libre * 2; et const total2 = donnee * 2;, lesquelles sont refusées par le compilateur TypeScript et lesquelles sont acceptées ? Justifiez à chaque fois en une phrase.
b) any et unknown acceptent tous les deux n’importe quelle valeur. Pourtant l’un des deux est considéré comme beaucoup plus sûr. Lequel, et pourquoi ? Illustrez votre réponse en montrant comment on peut malgré tout utiliser une variable unknown sans que le compilateur proteste.
c) Un utilisateur saisit le texte suivant dans le champ #saisie :
<img src="x" onerror="alert('piraté')">
Décrivez ce qui se produit à l’écran et expliquez pourquoi c’est un problème de sécurité. Proposez la correction (une seule propriété à changer) et expliquez précisément pourquoi elle protège.
Question 2 — Modules, fonctions fléchées et méthodes de tableaux
Fichier geometrie.ts :
export const PI = 3.14159;
export const aireCercle = (rayon: number): number => PI * rayon * rayon;
export const perimetreCercle = (rayon: number): number => 2 * PI * rayon;Fichier app.ts :
import { PI, aireCercle } from "./geometrie.ts";
const rayons: number[] = [1, 2, 3, 4, 5];
const resultat = rayons
.map((r) => aireCercle(r))
.filter((a) => a > 20)
.reduce((somme, a) => somme + a, 0);
console.log(resultat.toFixed(2));
const grand = rayons.find((r) => r > 10);
console.log(grand);a) perimetreCercle est exportée mais jamais importée. Est-ce une erreur ? Que se passerait-il si, à l’inverse, app.ts importait perimetreCercle sans que geometrie.ts ne l’exporte ?
b) Détaillez ce que produit la chaîne map → filter → reduce : quelles valeurs sont obtenues après chaque étape, et quel nombre est finalement affiché ? Expliquez le rôle du 0 passé en deuxième argument de reduce.
c) Que vaut grand et quel est son type exact ? Expliquez pourquoi TypeScript impose ce type de retour pour find, et pourquoi c’est une bonne chose du point de vue de la sûreté du code. Que se passerait-il si on écrivait directement grand.toFixed(2) ?
Question 3 — Programmation orientée objet
interface IVehicule {
getNom(): string;
getAutonomie(): number; // en km
}
abstract class Vehicule implements IVehicule {
protected nom: string;
private static compteur: number = 0;
constructor(nom: string) {
this.nom = nom;
Vehicule.compteur++;
}
getNom(): string {
return this.nom;
}
static getCompteur(): number {
return Vehicule.compteur;
}
abstract getAutonomie(): number;
}
class VoitureElectrique extends Vehicule {
private capaciteBatterie: number; // en kWh
constructor(nom: string, capaciteBatterie: number) {
super(nom);
this.capaciteBatterie = capaciteBatterie;
}
getAutonomie(): number {
return this.capaciteBatterie * 6;
}
}
class VeloElectrique extends Vehicule {
private capaciteBatterie: number;
constructor(nom: string, capaciteBatterie: number) {
super(nom);
this.capaciteBatterie = capaciteBatterie;
}
getAutonomie(): number {
return this.capaciteBatterie * 80;
}
}
const flotte: IVehicule[] = [
new VoitureElectrique("Zoe", 52),
new VeloElectrique("VanMoof", 0.5),
];
flotte.forEach((v) => console.log(`${v.getNom()} : ${v.getAutonomie()} km`));
console.log(Vehicule.getCompteur());a) Quelles lignes sont affichées par ce programme ? Que se passerait-il si on ajoutait const v = new Vehicule("Test"); et pourquoi ?
b) Lors de l’appel v.getAutonomie() dans le forEach, comment le programme détermine-t-il quelle version de la méthode exécuter ? Comment s’appelle ce mécanisme ?
c) Le tableau flotte est typé IVehicule[] et non Vehicule[]. Imaginez un nouveau type de véhicule qui ne peut pas hériter de Vehicule (par exemple parce qu’il provient d’une bibliothèque extérieure que vous ne pouvez pas modifier). Expliquez concrètement en quoi le choix de l’interface facilite son intégration dans la flotte.
Question 4 — JSON, généricité et exceptions
class Panier<T> {
private articles: T[] = [];
ajouter(article: T): void {
this.articles.push(article);
}
retirer(): T | undefined {
return this.articles.pop();
}
}
class ErreurStockVide extends Error {
constructor() {
super("Le panier est vide");
this.name = "ErreurStockVide";
}
}
interface IProduit {
nom: string;
prix: number;
afficher(): string;
}
class Produit implements IProduit {
nom: string;
prix: number;
constructor(nom: string, prix: number) {
this.nom = nom;
this.prix = prix;
}
afficher(): string {
return `${this.nom} : ${this.prix} euros`;
}
}
const p = new Produit("Clavier", 45);
const json = JSON.stringify(p);
const restaure: IProduit = JSON.parse(json);
console.log(restaure.afficher());a) Quelle chaîne est produite par JSON.stringify(p) ? Que se passe-t-il ensuite lors de l’appel restaure.afficher(), et à quel moment (compilation ou exécution) ? Expliquez la cause.
b) Cette situation est fréquente quand on récupère des données d’une API avec fetch. Proposez une manière de rendre le code fiable, et expliquez pourquoi le typage statique de TypeScript ne suffit pas à protéger contre ce type de bug.
c) On souhaite que retirer() lève ErreurStockVide quand le panier est vide, au lieu de retourner undefined. Écrivez la nouvelle version de retirer(), puis un bloc try/catch qui l’appelle et distingue ErreurStockVide des autres erreurs. Expliquez pourquoi l’ordre des tests instanceof a de l’importance dans ce bloc.
Question 5 — Asynchrone, HTTP, formulaires et Web Components
function chargerProfil(id: number): Promise<string> {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (id > 0) {
resolve(`Profil ${id}`);
} else {
reject(new Error("Identifiant invalide"));
}
}, 500);
});
}
async function afficher() {
console.log("2. Chargement...");
const profil = await chargerProfil(7);
console.log("4.", profil);
}
console.log("1. Début");
afficher();
console.log("3. Fin");a) Dans quel ordre les messages apparaissent-ils réellement dans la console ? Expliquez pourquoi "3. Fin" ne s’affiche pas en dernier, alors que la ligne se trouve à la fin du programme. Ajoutez à afficher() la gestion du cas où la promesse est rejetée.
b) Une application Astro appelle une API extérieure. Le fetch placé dans le frontmatter (entre les ---) fonctionne, mais le même fetch recopié dans un <script> de la page échoue avec une erreur CORS. Expliquez la différence entre ces deux emplacements, puis décrivez la technique vue en cours pour que le navigateur puisse quand même exploiter ces données.
c) Soit le Web Component et le formulaire suivants :
class BadgeUtilisateur extends HTMLElement {
static get observedAttributes(): string[] {
return ["pseudo"];
}
connectedCallback() { this.render(); }
attributeChangedCallback() { this.render(); }
render() {
this.textContent = `Bonjour ${this.getAttribute("pseudo")}`;
}
}
customElements.define("badge-utilisateur", BadgeUtilisateur);
const form = document.getElementById("monForm") as HTMLFormElement;
const input = document.getElementById("pseudoInput") as HTMLInputElement;
const badge = document.querySelector("badge-utilisateur") as BadgeUtilisateur;
form.addEventListener("submit", (event: Event) => {
badge.setAttribute("pseudo", input.value);
});À la soumission du formulaire, le badge se met à jour un très bref instant puis tout revient à l’état initial. Identifiez la cause exacte, donnez la ligne à ajouter et sa position. Expliquez ensuite le rôle de observedAttributes : que se passerait-il si on y retirait "pseudo" ?
Partie 2 — Exercice de codage
Durée indicative : 1h45 — environ 3 à 4 points par section
Contexte
Vous développez une petite application de catalogue de films. Les données vous sont fournies sous forme de chaîne JSON dans un module TypeScript.
Travaillez dans votre projet mon_projet pour écrire et tester votre code, puis recopiez le code de chaque section dans le fichier reponse_NOM.txt, entre les balises %%%DEBUT_S1%%%, %%%DEBUT_S2%%% et %%%DEBUT_S3%%%.
Un code partiellement fonctionnel rapporte des points : rendez toujours ce que vous avez écrit, même incomplet.
Organisation des fichiers dans votre projet
mon_projet/
└── src/
├── pages/
│ └── index.astro ← à remplacer par le fichier fourni ci-dessous
└── scripts/
├── catalogueFilms.ts ← à créer avec le contenu fourni ci-dessous
└── app.ts ← à remplacer par le squelette fourni ci-dessous
Fichier fourni 1 — src/scripts/catalogueFilms.ts
Créez ce fichier et copiez-y ce contenu sans le modifier.
export interface Film {
titre: string;
realisateur: string;
annee: number;
duree: number; // en minutes
genre: string;
note: number; // sur 10
}
export const filmsJSON: string = `[
{"titre":"Le Parrain","realisateur":"Francis Ford Coppola","annee":1972,"duree":175,"genre":"Drame","note":9.2},
{"titre":"Pulp Fiction","realisateur":"Quentin Tarantino","annee":1994,"duree":154,"genre":"Policier","note":8.9},
{"titre":"Inception","realisateur":"Christopher Nolan","annee":2010,"duree":148,"genre":"Science-Fiction","note":8.8},
{"titre":"Interstellar","realisateur":"Christopher Nolan","annee":2014,"duree":169,"genre":"Science-Fiction","note":8.6},
{"titre":"Kill Bill","realisateur":"Quentin Tarantino","annee":2003,"duree":111,"genre":"Action","note":8.2},
{"titre":"Le Voyage de Chihiro","realisateur":"Hayao Miyazaki","annee":2001,"duree":125,"genre":"Animation","note":8.6},
{"titre":"Princesse Mononoke","realisateur":"Hayao Miyazaki","annee":1997,"duree":134,"genre":"Animation","note":8.4},
{"titre":"Apocalypse Now","realisateur":"Francis Ford Coppola","annee":1979,"duree":147,"genre":"Guerre","note":8.5},
{"titre":"Dunkerque","realisateur":"Christopher Nolan","annee":2017,"duree":106,"genre":"Guerre","note":7.8},
{"titre":"Django Unchained","realisateur":"Quentin Tarantino","annee":2012,"duree":165,"genre":"Western","note":8.4}
]`;
// Chaîne volontairement mal formée, pour tester votre gestion d'erreurs
export const filmsJSON_invalide: string = `[
{"titre":"Erreur", realisateur: manquant}
`;
export const formatDuree = (minutes: number): string => {
const heures = Math.floor(minutes / 60);
const reste = minutes % 60;
const resteTexte = reste < 10 ? `0${reste}` : `${reste}`;
return `${heures}h${resteTexte}`;
};Fichier fourni 2 — src/pages/index.astro
Remplacez le contenu du fichier existant par celui-ci.
---
---
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>Catalogue de films</title>
<style>
body { font-family: sans-serif; max-width: 900px; margin: 20px auto; padding: 0 20px; }
h2 { border-bottom: 2px solid #8e44ad; padding-bottom: 5px; }
.resultat { background: #f9f9f9; border: 1px solid #ddd; padding: 10px; margin: 10px 0; }
table { width: 100%; border-collapse: collapse; }
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
th { background: #8e44ad; color: white; }
film-card { display: block; border: 1px solid #ccc; padding: 10px; margin: 5px 0; }
input, button { padding: 5px; }
</style>
</head>
<body>
<h1>Catalogue de films</h1>
<h2>Section 1 — Catalogue</h2>
<div id="catalogue" class="resultat"></div>
<h2>Section 2 — Fiches détaillées</h2>
<div id="fiches" class="resultat"></div>
<h2>Section 3 — Recherche et statistiques</h2>
<form id="rechercheForm">
<label for="rechercheInput">Réalisateur :</label>
<input type="text" id="rechercheInput" placeholder="Nolan, Tarantino..." />
<button type="submit">Rechercher</button>
</form>
<div id="resultatsRecherche" class="resultat"></div>
<div id="statistiques" class="resultat"></div>
<script src="../scripts/app.ts"></script>
</body>
</html>Fichier fourni 3 — src/scripts/app.ts (squelette à compléter)
C’est ce fichier que vous devez compléter. Remplacez le contenu du fichier existant par ce squelette.
// TODO 1 : importez depuis "./catalogueFilms.ts" :
// l'interface Film, les chaînes filmsJSON et filmsJSON_invalide, la fonction formatDuree
// =====================================================================
// SECTION 1 — Désérialisation JSON et affichage du catalogue
// =====================================================================
// TODO 2 : écrivez une fonction fléchée "parseFilms" qui prend une chaîne JSON
// et retourne un tableau de Film.
// - dans un try : parsez la chaîne et retournez le résultat
// - dans un catch : affichez l'erreur dans la console et retournez un tableau vide
// Appelez-la d'abord avec filmsJSON_invalide (pour vérifier que le programme
// ne plante pas), puis avec filmsJSON pour obtenir le tableau de films.
// TODO 3 : écrivez une fonction fléchée "creerLigne" qui prend un Film et retourne
// une chaîne HTML <tr> avec, dans l'ordre : titre, réalisateur, année,
// durée formatée avec formatDuree, genre, note.
// TODO 4 : avec map et join, construisez un tableau HTML complet (avec une ligne
// d'en-têtes) et affichez-le dans le div #catalogue.
const catalogueDiv = document.getElementById("catalogue") as HTMLDivElement;
// =====================================================================
// SECTION 2 — Classes, interface et généricité
// =====================================================================
// TODO 5 : écrivez une classe "FilmDetaille" qui implémente l'interface Film
// (toutes ses propriétés) et qui ajoute :
// - un constructeur prenant un objet Film en paramètre et recopiant ses propriétés
// - une méthode "estRecent(): boolean" qui retourne true si annee >= 2010
// - une méthode "getResume(): string" qui retourne une chaîne de la forme :
// "Titre (année) de Réalisateur — note/10"
// TODO 6 : écrivez une classe générique "Collection<T>" avec :
// - une propriété privée elements de type T[]
// - une méthode "ajouter(element: T): void"
// - une méthode "getTous(): T[]"
// - une méthode "taille(): number"
// TODO 7 : créez une Collection<FilmDetaille>. Parcourez le tableau de films
// de la section 1 et ajoutez-y, pour chacun, une instance de FilmDetaille.
// Affichez ensuite dans le div #fiches, sous forme de liste <ul><li> :
// - le résumé de chaque film (getResume())
// - suivi de " [récent]" uniquement si estRecent() vaut true
// Terminez par une ligne indiquant le nombre total de films de la collection.
const fichesDiv = document.getElementById("fiches") as HTMLDivElement;
// =====================================================================
// SECTION 3 — Web Component, formulaire et statistiques asynchrones
// =====================================================================
// TODO 8 : créez un Web Component <film-card> (classe FilmCard héritant de HTMLElement)
// qui observe les attributs "titre" et "realisateur", et dont la méthode render()
// affiche : <strong>TITRE</strong> — <em>REALISATEUR</em>
// Enregistrez-le avec customElements.define.
// TODO 9 : gérez la soumission du formulaire #rechercheForm (sans recharger la page).
// - récupérez la valeur saisie dans #rechercheInput
// - avec filter, sélectionnez les films dont le réalisateur contient cette valeur
// (recherche insensible à la casse)
// - videz le div #resultatsRecherche, puis, avec forEach, créez un élément
// <film-card> par film trouvé et ajoutez-le au div
// - si aucun film ne correspond, affichez le texte "Aucun film trouvé"
const rechercheForm = document.getElementById("rechercheForm") as HTMLFormElement;
const resultatsDiv = document.getElementById("resultatsRecherche") as HTMLDivElement;
// TODO 10 : écrivez une fonction "calculerStatistiques" qui retourne une
// Promise<string> résolue après 1 seconde (utilisez setTimeout).
// La chaîne résolue contient, calculés avec reduce :
// - la durée totale de tous les films, formatée avec formatDuree
// - la note moyenne de tous les films, avec 2 décimales
// Puis écrivez une fonction asynchrone qui affiche d'abord "Calcul en cours..."
// dans le div #statistiques, attend le résultat avec await, et l'affiche.
// Appelez cette fonction asynchrone.
const statsDiv = document.getElementById("statistiques") as HTMLDivElement;Travail demandé — détail
Section 1 — Désérialisation JSON et affichage (environ 3 points)
Concepts évalués : modules, JSON, exceptions, fonctions fléchées, map, manipulation du DOM.
- Import depuis le module
catalogueFilms.ts parseFilms: parsing sécurisé partry/catch, retour d’un tableau vide en cas d’échec- Vérification que le programme survit à
filmsJSON_invalide creerLigne: génération d’une ligne<tr>à partir d’unFilm- Affichage d’un tableau HTML complet avec en-têtes dans
#catalogue
Section 2 — Classes, interface et généricité (environ 3 points)
Concepts évalués : interfaces, classes, méthodes, généricité, forEach.
FilmDetailleimplémentantFilm, avecestRecent()etgetResume()Collection<T>générique avecajouter,getTous,taille- Remplissage de la collection et affichage de la liste dans
#fiches - Mention
[récent]pour les films de 2010 ou après, et total en fin de liste
Section 3 — Web Component, formulaire et asynchrone (environ 4 points)
Concepts évalués : Web Components, formulaires, filter, reduce, promesses, async/await.
- Web Component
<film-card>avecobservedAttributes,connectedCallback,attributeChangedCallbacketrender - Formulaire de recherche par réalisateur, sans rechargement de page, avec création dynamique des
<film-card> - Message
"Aucun film trouvé"le cas échéant calculerStatistiquesretournant une promesse résolue après 1 seconde- Durée totale et note moyenne calculées avec
reduce, affichées aprèsawait
Rappel du fichier de réponses
Le fichier reponse_NOM.txt fourni dans l’outillage a la structure suivante. En cas de perte, vous pouvez le recréer à l’identique.
%%%DEBUT_IDENTITE%%%
NOM :
PRENOM :
%%%FIN_IDENTITE%%%
%%%DEBUT_Q1%%%
a)
b)
c)
%%%FIN_Q1%%%
%%%DEBUT_Q2%%%
...
%%%FIN_Q2%%%
%%%DEBUT_Q3%%%
...
%%%FIN_Q3%%%
%%%DEBUT_Q4%%%
...
%%%FIN_Q4%%%
%%%DEBUT_Q5%%%
...
%%%FIN_Q5%%%
%%%DEBUT_S1%%%
(code de la section 1)
%%%FIN_S1%%%
%%%DEBUT_S2%%%
(code de la section 2)
%%%FIN_S2%%%
%%%DEBUT_S3%%%
(code de la section 3)
%%%FIN_S3%%%
%%%DEBUT_REMARQUES%%%
(facultatif)
%%%FIN_REMARQUES%%%
Bon courage !
Comment rendre votre copie
Un seul fichier
Vous rendez un seul fichier texte : le fichier
reponse_NOM.txtfourni dans l’outillage.outillage/reponse_NOM.txtdans votre éditeur.reponse_DUPONT.txt.Les balises
Le fichier contient des balises de ce type :
Ne modifiez pas, ne déplacez pas et ne supprimez pas les balises. Elles servent à retrouver chacune de vos réponses. Une balise abîmée ou manquante rend la réponse introuvable, et donc non notée.
Les balises attendues sont :
%%%DEBUT_IDENTITE%%%…%%%FIN_IDENTITE%%%%%%DEBUT_Q1%%%…%%%FIN_Q1%%%%%%DEBUT_Q2%%%…%%%FIN_Q2%%%%%%DEBUT_Q3%%%…%%%FIN_Q3%%%%%%DEBUT_Q4%%%…%%%FIN_Q4%%%%%%DEBUT_Q5%%%…%%%FIN_Q5%%%%%%DEBUT_S1%%%…%%%FIN_S1%%%%%%DEBUT_S2%%%…%%%FIN_S2%%%%%%DEBUT_S3%%%…%%%FIN_S3%%%%%%DEBUT_REMARQUES%%%…%%%FIN_REMARQUES%%%Votre responsabilité
Ctrl+S). Des copies ont déjà été perdues à quelques minutes de la fin.