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

Facile ⏱ 5 min

🎯 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 ?

Facile ⏱ 6 min

🎯 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

Intermédiaire ⏱ 8 min

🎯 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))
  1. Exécutez parseWithAny(42) : où l’erreur apparaît-elle ?
  2. Réparez parseWithUnknown avec un typeof, en renvoyant la chaîne en majuscules ou null.

any déplace l’erreur du build vers la production. unknown la garde au build.

Exercice 4 — Typer une fonction

Facile ⏱ 6 min

🎯 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

Facile ⏱ 8 min

🎯 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

Intermédiaire ⏱ 10 min

🎯 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
}
  1. Écrivez area. Dans chaque branche, survolez shape : TypeScript a restreint le type tout seul.
  2. Ajoutez type Triangle à l’union sans toucher à area. Que se passe-t-il ?
  3. Ajoutez un default qui fait const _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

Intermédiaire ⏱ 8 min

🎯 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

Intermédiaire ⏱ 8 min

🎯 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

Intermédiaire ⏱ 8 min

🎯 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 ?
  1. Écrivez damage(player, amount) qui retourne un nouveau joueur.
  2. 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é

Difficile ⏱ 60 min

🎯 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.

  • createMatch retourne toujours un PendingMatch
  • resolveMatch prend un PendingMatch et retourne toujours un FinishedMatch
  • extractWinner ne manipule plus que des FinishedMatch

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.