Typescript
Ces exercices sont ceux de la séance : ils sont intercalés dans les slides, juste après le concept qu’ils font travailler. Cette page les rassemble pour que vous puissiez les reprendre à votre rythme.
Les neuf premiers sont courts : ils font manipuler une notion chacun, directement dans les slides. Le dixième est le mini-projet de la séance : comptez la seconde moitié de la séance pour lui seul.
Nous utiliserons CodeSandbox (fonctionne mieux avec Chrome/Chromium). Notez que les blocs de code des slides vérifient les types mais ne s’exécutent pas : Typescript ne s’exécute nulle part, il se compile.
La documentation de Typescript est ici, celle de Javascript là.
Exercice 1 — Annoter et laisser deviner
🎯 Voir où l'annotation est utile, et où elle est du bruit.
Corrigez ce qui ne compile pas, puis supprimez les annotations dont TypeScript n’a pas besoin.
let age: number = "31"
let firstname: string = "Julien"
let isAdmin: boolean = 1
let city = "Clermont"
city = 63
Survolez city : quel type TypeScript lui a-t-il donné tout seul ?
Exercice 2 — Tableau ou tuple ?
🎯 Choisir entre « plusieurs fois la même chose » et « une structure fixe ».
// 1. Un tableau de nombres, puis le même en syntaxe Array<...>
// 2. Un tableau de chaînes qui refuse un nombre
// 3. Un tuple [nom, âge] — et vérifiez qu'il refuse [31, "Julien"]
const coordinates: [number, number] = [45.7, 3.1]
coordinates.push(99) // 😱 pourquoi est-ce accepté ?
La dernière ligne est un trou connu du typage des tuples. Que se passe-t-il si vous déclarez le tuple readonly ?
Exercice 3 — any contre unknown
🎯 Comprendre pourquoi `unknown` est pénible — et pourquoi c'est une qualité.
function parseWithAny(data: any) {
return data.toUpperCase() // compile. Et à l'exécution ?
}
function parseWithUnknown(data: unknown) {
return data.toUpperCase() // ❌ ne compile pas
}
console.log(parseWithAny(42))
- Exécutez
parseWithAny(42): où l’erreur apparaît-elle ? - Réparez
parseWithUnknownavec untypeof, en renvoyant la chaîne en majuscules ounull.
any déplace l’erreur du build vers la production. unknown la garde au build.
Exercice 4 — Typer une fonction
🎯 Paramètres, retour, et paramètre facultatif.
// 1. `divide(a, b)` retourne un number, et throw si b vaut 0
// 2. `greet(name?)` affiche "Hello World" quand on ne passe rien
// 3. Que devient le type de retour si vous l'omettez sur divide ?
function divide(a, b) {
return a / b
}
Enlevez le type de retour de divide et survolez-la : TypeScript l’infère. Alors pourquoi l’écrire ? Parce qu’il vous prévient si le corps cesse de correspondre.
Exercice 5 — Décrire la forme d'un objet
🎯 Passer d'un type inline à une interface réutilisable.
// 1. Déclarez `user` avec un type inline { firstname, lastname, age }
// 2. Sortez cette forme dans une interface `User`, et réutilisez-la
// 3. Ajoutez `email` en propriété facultative
// 4. Créez `interface Admin extends User` avec `permissions: string[]`
// 5. Que dit TypeScript si vous ajoutez une propriété non déclarée ?
Le point 5 s’appelle le excess property check : il ne s’applique qu’aux objets littéraux assignés directement. Essayez en passant par une variable intermédiaire.
Exercice 6 — L'union discriminée
🎯 Le motif le plus utile de TypeScript — et celui du mini-projet.
type Circle = { kind: "circle"; radius: number }
type Square = { kind: "square"; side: number }
type Shape = Circle | Square
function area(shape: Shape): number {
// switch sur shape.kind
}
- Écrivez
area. Dans chaque branche, survolezshape: TypeScript a restreint le type tout seul. - Ajoutez
type Triangleà l’union sans toucher àarea. Que se passe-t-il ? - Ajoutez un
defaultqui faitconst _exhaustif: never = shape. Maintenant, oublier un cas devient une erreur de compilation.
C’est exactement la structure Warrior | Mage du mini-projet.
Exercice 7 — Une fonction pour tous les types
🎯 Réutiliser sans perdre le typage, et sans `any`.
// 1. `first(array)` retourne le premier élément, ou undefined si vide.
// Le type de retour doit suivre celui du tableau.
function first(array) {
return array[0]
}
const n = first([1, 2, 3]) // doit être number | undefined
const s = first(["a", "b"]) // doit être string | undefined
// 2. Écrivez la même chose avec `any` : que perd-on sur `n` et `s` ?
Bonus : pluck<T, K extends keyof T>(items: T[], key: K) qui retourne le tableau des valeurs de cette clé.
Exercice 8 — Dériver un type d'un autre
🎯 Ne jamais réécrire à la main un type qui existe déjà.
interface User {
id: number
name: string
email: string
password: string
}
// 1. `PublicUser` : User sans le password
// 2. `UserPreview` : seulement id et name
// 3. `UserUpdate` : toutes les propriétés facultatives, sauf l'id qui reste requis
// 4. `ReadonlyUser` : toutes les propriétés en lecture seule
Ajoutez une propriété à User : combien de vos types doivent être modifiés à la main ? C’est tout l’intérêt.
Exercice 9 — Interdire la mutation au build
🎯 Faire vérifier par le compilateur ce qu'on s'imposait à la main en Javascript 4.
type Player = {
readonly id: number
readonly name: string
readonly hp: number
}
const player: Player = { id: 1, name: "Alice", hp: 100 }
player.hp = 50 // ❌ que dit le compilateur ?
const players: Player[] = [player]
players.push(player) // ✅ accepté — pourquoi ?
- Écrivez
damage(player, amount)qui retourne un nouveau joueur. - Faites en sorte que
players.push(...)soit refusé lui aussi.
En Javascript 4, l’immutabilité était une discipline. Ici, c’est le compilateur qui la fait respecter.
Exercice 10 — Mini-projet — un tournoi typé
🎯 Assembler unions discriminées, readonly et utility types sur un vrai programme.
Un tournoi à élimination directe : une liste de joueurs, chaque tour produit une nouvelle liste, jusqu’au vainqueur.
Règles du jeu : ❌ mutation · ❌ any · ❌ ! · ❌ as — et toute erreur doit être bloquée au build ou au runtime (throw new Error()). Les console.log() sont vos amis.
Étape 1 — Modéliser les joueurs
type BasePlayer = {
readonly id: number;
readonly name: string;
readonly level: number;
readonly hp: number;
};
type Warrior = BasePlayer & {
readonly role: "WARRIOR";
readonly strength: number;
};
Créer Mage (avec mana), puis Player = Warrior | Mage, puis displayPlayer qui affiche les infos — avec un switch sur role.
Étape 2 — Calculer la puissance
function getPower(player: Player): number;
WARRIOR → level + strength, MAGE → level + mana.
Étape 3 — Modéliser les matchs
type Match = {
readonly id: number;
readonly player1: Player;
readonly player2: Player;
readonly winner?: Player;
};
Plus une fonction displayMatch.
Étape 4 — Créer un match
function createMatch(id: number, player1: Player, player2: Player): Match;
Avec player1.id !== player2.id.
Étape 5 — Résoudre un match
function resolveMatch(match: Match): Match;
Sans winner, on le calcule : la plus grande puissance gagne, et à égalité, le plus de hp. Avec un winner, on retourne le match tel quel. Le vainqueur est forcément player1 ou player2.
Étape 6 — Extraire le vainqueur
function extractWinner(match: Match): Player;
Le match doit avoir un winner.
Étape 7 — Un tour
function playRound(players: readonly Player[]): readonly Player[];
Nombre de joueurs pair, appariement séquentiel : création → résolution → extraction.
Étape 8 — Le tournoi
function runTournament(players: readonly Player[]): Player;
Appelle playRound tant qu’il reste plus d’un joueur, et retourne le vainqueur.
Étape 9 — La refacto qui justifie TypeScript
Remplacer winner?: Player par deux types distincts : un match est soit PendingMatch, soit FinishedMatch — et seul le second a un winner.
createMatchretourne toujours unPendingMatchresolveMatchprend unPendingMatchet retourne toujours unFinishedMatchextractWinnerne manipule plus que desFinishedMatch
Comptez les vérifications que vous pouvez supprimer après cette étape : le compilateur cesse de vous demander de prouver ce que la signature garantit déjà. C’est l’étape la plus intéressante des dix.
Étape 10 — Bonus
Une nouvelle classe de joueur (archer, soigneur…), un système d’expérience et de niveaux, une interface pour visualiser le tournoi.