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.


Comment rendre votre copie

Un seul fichier

Vous rendez un seul fichier texte : le fichier reponse_NOM.txt fourni dans l’outillage.

  1. Ouvrez outillage/reponse_NOM.txt dans votre éditeur.
  2. Renommez-le avec votre nom de famille en majuscules : reponse_DUPONT.txt.
  3. Écrivez toutes vos réponses (théorie et code) entre les balises prévues.
  4. Déposez ce fichier sur tp.enst.fr.

Les balises

Le fichier contient des balises de ce type :

%%%DEBUT_Q1%%%

(votre réponse ici)

%%%FIN_Q1%%%
ImportantNe modifiez pas les balises

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 :

Balise Contenu attendu
%%%DEBUT_IDENTITE%%% … %%%FIN_IDENTITE%%% Vos nom et prénom
%%%DEBUT_Q1%%% … %%%FIN_Q1%%% Réponse à la question 1 (parties a, b, c)
%%%DEBUT_Q2%%% … %%%FIN_Q2%%% Réponse à la question 2
%%%DEBUT_Q3%%% … %%%FIN_Q3%%% Réponse à la question 3
%%%DEBUT_Q4%%% … %%%FIN_Q4%%% Réponse à la question 4
%%%DEBUT_Q5%%% … %%%FIN_Q5%%% Réponse à la question 5
%%%DEBUT_S1%%% … %%%FIN_S1%%% Code de la section 1
%%%DEBUT_S2%%% … %%%FIN_S2%%% Code de la section 2
%%%DEBUT_S3%%% … %%%FIN_S3%%% Code de la section 3
%%%DEBUT_REMARQUES%%% … %%%FIN_REMARQUES%%% Remarques libres (facultatif)

Votre responsabilité

  • Enregistrez très régulièrement (Ctrl+S). Des copies ont déjà été perdues à quelques minutes de la fin.
  • Vérifiez avant l’envoi que toutes vos réponses sont bien dans le fichier, et bien entre les bonnes balises.
  • Tout ce qui se trouve en dehors de ce fichier ne sera pas corrigé : votre projet Astro sert uniquement à écrire et tester votre code, il n’est pas ramassé.
  • Vous pouvez déposer le fichier plusieurs fois : seul le dernier dépôt est conservé.

Mise en place (5 minutes)

Téléchargez l’archive outillage.tar.gz depuis la page Outillage et décompressez-la.

  1. Copiez le dossier outillage/projet_template en entier, renommez la copie mon_projet (à la souris, ou avec cp -R outillage/projet_template mon_projet — -R en majuscule).
  2. Dans un terminal : cd mon_projet puis npx astro dev
  3. Ouvrez http://localhost:4321/ dans le navigateur.
  4. Vos fichiers .astro vont dans src/pages/, vos fichiers .ts dans src/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%%%.

NoteUn code partiel rapporte des points

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é par try/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’un Film
  • 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.

  • FilmDetaille implémentant Film, avec estRecent() et getResume()
  • Collection<T> générique avec ajouter, 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> avec observedAttributes, connectedCallback, attributeChangedCallback et render
  • 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
  • calculerStatistiques retournant une promesse résolue après 1 seconde
  • Durée totale et note moyenne calculées avec reduce, affichées après await

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 !

👉 Corrigé de l’évaluation 3