Retour au cours

frontend / typescript

Internals du compilateur

Leçon 221 exercice

Explication

Ce que vous allez apprendre

  • Comprendre le typage structurel (duck typing) qui régit toutes les comparaisons de types de TypeScript
  • Reconnaître l'excess property check et savoir pourquoi il ne s'applique qu'aux littéraux directs
  • Comprendre la variance des paramètres de fonction en position contravariante
  • Simuler un typage nominal avec la technique du branding pour distinguer deux types structurellement identiques
  • Comprendre l'effacement total des types à l'exécution (type erasure) et ses conséquences pratiques

Dans quel contexte ?

Une équipe backend manipule à la fois un UserId et un ProductId, tous deux de simples chaînes de caractères. Un développeur, par erreur d'inattention, appelle chargerUtilisateur(produitId) au lieu de chargerUtilisateur(userId) : comme les deux valeurs sont structurellement identiques (de simples string), TypeScript ne détecte rien, et l'application charge silencieusement les mauvaises données. Cette dernière leçon du cours explique pourquoi ce genre d'erreur passe inaperçu par défaut, et la technique (le branding) qui permet de le rendre détectable à la compilation.

Comment TypeScript compare-t-il réellement les types ?

Cette dernière leçon lève le voile sur un mécanisme utilisé implicitement depuis le début du cours : TypeScript compare les types par leur structure, on parle de typage structurel ou duck typing (si ça marche comme un canard, c'est un canard), et non par leur nom déclaré. Deux interfaces différentes mais possédant exactement les mêmes propriétés sont totalement interchangeables, même sans aucun lien de parenté explicite entre elles.

Prérequis

Cette dernière leçon relit avec un regard plus théorique des notions déjà utilisées tout au long du cours (interfaces, unions, génériques) : elle n'introduit pas de nouvelle syntaxe à proprement parler, mais explique le mécanisme qui les rend toutes cohérentes entre elles.

Un contrôle plus strict, mais uniquement sur les littéraux directs

Une exception notable à cette souplesse : quand on passe un objet littéral directement à une fonction, TypeScript refuse les propriétés en trop, c'est l'excess property check. Mais dès que ce même objet transite par une variable intermédiaire, cette vérification stricte disparaît — un détail subtil qui explique certains comportements qui semblent, à première vue, incohérents.

SituationExcess property check appliqué ?
Objet littéral passé directement en argumentOui — propriétés en trop refusées
Objet stocké dans une variable puis passé en argumentNon — comparaison structurelle classique
Type nominal simulé par brandingSans objet, distinction garantie par une propriété fantôme

Piège fréquent

Deux types structurellement identiques (par exemple deux alias de string) restent totalement interchangeables par défaut, même s'ils représentent des concepts métier différents comme un identifiant utilisateur et un identifiant produit. Sans branding, TypeScript ne peut tout simplement pas détecter une confusion entre les deux.

Simuler un typage nominal avec le branding

Le typage structurel a une limite : deux types différents mais structurellement identiques, un identifiant utilisateur et un identifiant produit, tous deux de simples chaînes, sont interchangeables alors qu'ils ne devraient jamais l'être en pratique. La technique du branding contourne ce problème en ajoutant une propriété fantôme, invisible à l'exécution, qui rend chaque type distinct aux yeux du compilateur.

L'effacement total au runtime

Le message final du cours est peut-être le plus important : tous les types disparaissent intégralement à la compilation. Aucune trace de vérification de type ne subsiste dans le JavaScript généré. C'est pourquoi valider réellement des données externes, comme une réponse d'API ou un formulaire utilisateur, nécessite des bibliothèques comme zod ou valibot, qui effectuent elles de vraies vérifications à l'exécution. TypeScript et la validation runtime sont complémentaires, jamais interchangeables.

Commandes & code

Internals du compilateur

ts
// Typage STRUCTUREL (duck typing), pas nominal : deux types avec la même
// forme sont interchangeables, même sans lien de parenté déclaré.
interface PointA {
  x: number;
  y: number;
}
interface PointB {
  x: number;
  y: number;
}
const a: PointA = { x: 1, y: 2 };
const b: PointB = a; // OK : structure identique, aucune erreur

// Excess property check : ne s'applique QUE sur un littéral d'objet direct
function afficherPoint(p: PointA) {
  /* ... */
}
// afficherPoint({ x: 1, y: 2, z: 3 }); // Error : "z" n'existe pas sur PointA (littéral direct)
const point = { x: 1, y: 2, z: 3 };
afficherPoint(point); // OK : passé via une variable, pas de vérification stricte (structurel)

// Variance des fonctions : un paramètre de fonction est vérifié en position CONTRAVARIANTE
type Gestionnaire<T> = (valeur: T) => void;
let gererAnimal: Gestionnaire<{ nom: string }> = (a) => console.log(a.nom);
let gererChien: Gestionnaire<{ nom: string; race: string }>;
gererChien = gererAnimal; // OK avec strictFunctionTypes : un handler plus général convient
// gererAnimal = gererChien; // Error : gererChien exige "race", incompatible pour un usage plus large

// Nominal typing "simulé" via une marque privée (branding), utile pour distinguer
// deux types structurellement identiques mais sémantiquement différents.
type UserId = string & { readonly __brand: "UserId" };
type ProductId = string & { readonly __brand: "ProductId" };
function creerUserId(valeur: string): UserId {
  return valeur as UserId;
}
function chargerUtilisateur(id: UserId) {
  /* ... */
}
const uid = creerUserId("u-123");
chargerUtilisateur(uid); // OK
// chargerUtilisateur("u-123"); // Error : string brut n'est pas UserId
// chargerUtilisateur(produitId); // Error même si produitId est aussi un "string & {brand}"

// Types récursifs : limités par la profondeur d'instanciation du compilateur (environ 50 niveaux)
type Json = string | number | boolean | null | Json[] | { [cle: string]: Json };
const document: Json = { titre: "Article", tags: ["ts", "compilateur"], meta: { vues: 42 } };

// Le mot-clé "declare" ne génère AUCUN JS : il décrit une forme déjà existante à l'exécution
declare const VERSION_BUILD: string; // injectée par le bundler (define/replace), pas par TS

// Compilation = simple EFFACEMENT des types (erasure) : aucune vérification de type
// ne subsiste au runtime. D'où l'intérêt de "satisfies", des type guards et de librairies
// comme zod/valibot pour valider de vraies données externes que TS ne peut pas garantir.
function estUtilisateur(valeur: unknown): valeur is { id: number; nom: string } {
  return (
    typeof valeur === "object" &&
    valeur !== null &&
    "id" in valeur &&
    "nom" in valeur &&
    typeof (valeur as any).id === "number" &&
    typeof (valeur as any).nom === "string"
  );
}

Résumé

  • TypeScript compare les types par STRUCTURE, jamais par nom déclaré (duck typing).
  • L'excess property check ne s'applique qu'aux littéraux d'objets passés directement.
  • Le branding (string & { __brand }) simule un typage nominal quand la structure ne suffit pas.
  • Les types sont totalement effacés au runtime : valider les données externes reste indispensable.

Exercices pratiques

1 disponible
1

Mission : empêcher la confusion entre UserId et ProductId

Objectif : Comprendre les limites du typage structurel sur un cas concret, puis simuler un typage nominal avec le branding pour rendre une confusion d'identifiants détectable à la compilation.

Contexte

Une équipe backend manipule un UserId et un ProductId, tous deux de simples chaînes de caractères. Un développeur appelle par erreur chargerUtilisateur(produitId) au lieu de chargerUtilisateur(userId) : comme les deux valeurs sont structurellement identiques, TypeScript ne détecte rien. Ta mission : rendre cette confusion impossible à compiler.

Résoudre l’exercice →