Développement Web

TypeScript moderne : du JS au système de types industriel

21 دقائق للقراءة

Sommaire

Pourquoi TypeScript s’est imposé

JavaScript a une qualité rare : il tourne partout, du navigateur le plus modeste à un cluster de microservices. C’est aussi sa fragilité. Sans déclarations de types, chaque appel de fonction, chaque accès à une propriété, chaque transformation de données est un pari fait à l’exécution. Sur un script de cent lignes, le pari est tenable. Sur un produit qui dépasse vingt mille lignes, partagé par plusieurs équipes, déployé en continu, le pari devient un coût quotidien : régressions silencieuses, refactorisations risquées, autocomplétion approximative, documentation vivante absente. TypeScript a été conçu pour remettre la lisibilité, la maintenabilité et la sûreté au cœur du langage tout en gardant l’écosystème intact.

Depuis sa première version stable en 2014, TypeScript est passé du statut de surcouche optionnelle à celui de standard de fait sur les bases de code professionnelles. Les principales bibliothèques côté frontend (React, Vue, Angular, Svelte) et côté backend (NestJS, Fastify, Drizzle, Prisma) sont écrites en TypeScript ou en exposent des typages officiels. Les outils de build (Vite, esbuild, swc, turbo) ont tous une intégration de premier ordre. Et depuis la version 5.0 sortie en mars 2023, le compilateur lui-même a entamé un cycle de modernisation qui a abouti à TypeScript 6.0 le 23 mars 2026, avec strict activé par défaut et le passage à ES2025 comme cible standard, puis à une réécriture native en Go annoncée comme TypeScript 7.0 Beta le 21 avril 2026, environ dix fois plus rapide que la pile JavaScript actuelle.

L’objet de cette page est de fournir une vue d’ensemble structurée : ce que TypeScript est exactement, comment son système de types s’articule, quels sont les outils essentiels à maîtriser, et par où démarrer pour devenir productif. Chaque section principale renvoie vers un tutoriel pas-à-pas qui creuse le sujet avec du code testé sur la version stable courante.

Le modèle mental : un typage statique sans runtime

La première chose à comprendre, et celle qui prête le plus souvent à confusion, est que TypeScript n’existe qu’à la compilation. Quand vous lancez tsc, le compilateur lit votre code, vérifie chaque expression, signale les incohérences, puis émet du JavaScript débarrassé de toute annotation. À l’exécution, il n’y a plus de TypeScript : ni vérification, ni introspection, ni cast dynamique. C’est exactement le même JavaScript que celui que vous écririez à la main, simplement obtenu par traduction depuis une source enrichie.

Ce choix d’effacement total a deux conséquences importantes. La première est que TypeScript n’introduit aucune pénalité d’exécution. Le code émis est aussi rapide que du JavaScript natif, parce que c’est littéralement du JavaScript. La seconde est que toutes les vérifications doivent tenir avant l’exécution. Si vous lisez une réponse HTTP et que vous affirmez qu’elle correspond à un type User, TypeScript vous croit sur parole. À ce stade, le système de types ne sait pas si le serveur a vraiment renvoyé un User ; il sait seulement que vous l’avez affirmé. C’est la raison pour laquelle, sur les frontières du système (réseau, lecture de fichier, formulaire utilisateur, variables d’environnement), on combine TypeScript avec un validateur de schéma comme Zod, Valibot ou ArkType pour faire coïncider le contrat statique avec la réalité dynamique.

Le second pilier du modèle mental est le typage structurel. Contrairement à Java ou C# qui pratiquent un typage nominal, TypeScript considère deux types comme compatibles dès lors qu’ils possèdent la même forme. Une fonction qui attend un { name: string } accepte tout objet qui contient au moins une propriété name de type chaîne, peu importe si cet objet a été construit à partir d’une classe nommée User, d’un littéral anonyme ou d’une déconstruction. Ce comportement est plus permissif qu’un typage nominal, mais il colle à la façon dont les développeurs JavaScript pensent depuis toujours : la forme compte plus que la lignée.

Les fondations du système de types

Quand on apprend TypeScript, on traverse trois couches successives. La première est celle des annotations explicites : on indique au compilateur le type attendu pour un paramètre, une variable, une propriété ou une valeur de retour. La deuxième est celle de l’inférence : on laisse TypeScript déduire seul les types à partir du contexte, ce qui réduit drastiquement la verbosité. La troisième est celle du narrowing : on profite des contrôles qu’on écrit déjà en JavaScript (typeof, in, instanceof, comparaisons d’égalité, gardes utilisateur) pour qu’à l’intérieur d’un bloc, le type d’une variable se rétrécisse automatiquement.

function describe(input: string | number) {
  if (typeof input === "string") {
    // Ici, TypeScript sait que input est une chaîne.
    return input.toUpperCase();
  }
  // Ici, TypeScript sait que input est forcément un number.
  return input.toFixed(2);
}

Cet exemple illustre la philosophie générale : on écrit du JavaScript naturel, et TypeScript suit le raisonnement. Aucune cast manuel, aucun as nécessaire. Le pattern se généralise aux unions discriminées, où chaque variante porte un champ littéral qui sert d’étiquette, et qu’on consomme dans un switch exhaustif. Le compilateur, en lisant le case, rétrécit le type à la variante correspondante. C’est l’un des outils les plus puissants pour modéliser des états applicatifs (chargement, succès, erreur), des messages d’événements, ou des protocoles de communication.

Sur les primitives, TypeScript apporte une précision dont JavaScript est dépourvu : les literal types. Une variable typée "GET" ne pourra contenir que la chaîne exacte "GET", pas n’importe quelle chaîne. Combiné aux unions, on obtient des types comme "GET" | "POST" | "PUT" | "DELETE" qui interdisent toute valeur autre, et qui se prêtent ensuite à l’autocomplétion dans l’éditeur. Cette mécanique est à la base des template literal types apparus en 4.1, qui permettent de manipuler des chaînes au niveau du système de types — par exemple pour construire des routes typées ou des noms d’événements préfixés.

Type aliases et interfaces

Deux constructions permettent de donner un nom à une forme : type et interface. Pour décrire un simple objet, elles sont presque équivalentes. La différence apparaît dès qu’on s’éloigne du cas trivial. Une interface ne peut décrire qu’une forme d’objet, mais elle accepte la déclaration multiple : si vous déclarez interface User à deux endroits, TypeScript fusionne automatiquement les propriétés. Un alias de type, à l’inverse, peut décrire absolument n’importe quoi — primitive, union, tuple, intersection — mais ne tolère pas la redéclaration.

// Interface : extensible par fusion
interface Person { name: string }
interface Person { age: number }
// Person vaut maintenant { name: string; age: number }

// Type alias : décrit toutes les formes
type Status = "idle" | "loading" | "success" | "error";
type Pair = [string, number];
type Nullable<T> = T | null;

La recommandation pragmatique est simple : utilisez interface par défaut pour les formes d’objet exposées dans une API publique, parce que l’extensibilité par fusion permet aux consommateurs de votre bibliothèque d’ajouter des propriétés (c’est ce que font les types Window ou Express.Request par exemple). Utilisez type pour tout ce qui n’est pas un objet, et pour les compositions complexes (intersections, conditionals, mapped types). Le tutoriel dédié creuse ces nuances avec des cas où le choix entre les deux a des conséquences concrètes : voir Types vs interfaces.

Génériques : la programmation paramétrique

Les génériques transforment une fonction ou un type d’une signature fixe en une signature paramétrique. Plutôt que d’écrire dix versions d’un même cache adapté à dix types de valeurs, on écrit une seule signature Cache<T> et le compilateur inférera T en fonction de l’usage. C’est le mécanisme qui rend possibles les structures de données réutilisables, les bibliothèques de validation, les ORM type-safe et les routeurs HTTP typés.

function identity<T>(value: T): T {
  return value;
}

const n = identity(42);        // T inféré : number
const s = identity("bonjour"); // T inféré : string

// Contraintes : T doit avoir une propriété length
function logLength<T extends { length: number }>(arg: T): T {
  console.log(arg.length);
  return arg;
}

Les évolutions récentes ont enrichi cette mécanique. TypeScript 5.0 a introduit les const type parameters qui forcent l’inférence en mode as const, utiles pour préserver les littéraux dans les API qui en dépendent. TypeScript 5.4 a apporté NoInfer<T>, qui bloque l’inférence sur un paramètre lorsqu’on veut qu’il soit choisi par un autre. Les conditional types (T extends U ? X : Y) et le mot-clé infer permettent d’extraire des sous-types à la volée, mécanisme sous-jacent à la plupart des utility types fournis par la bibliothèque standard.

Les génériques deviennent vite la zone où l’on ressent vraiment la puissance du système de types, mais aussi celle où l’on bascule le plus vite dans l’incompréhensible si on n’a pas un cadre d’apprentissage solide. Le tutoriel dédié construit cette compétence étape par étape, des cas triviaux aux patterns avancés comme l’inférence sur tuples variadiques : voir Génériques avancés.

Utility types et mapped types

Une fois les génériques compris, on découvre que TypeScript expose une bibliothèque standard d’utility types qui permettent de dériver de nouveaux types à partir de types existants sans les répéter. Partial<T> rend toutes les propriétés optionnelles, Required<T> fait l’inverse, Readonly<T> verrouille les propriétés, Pick<T, K> et Omit<T, K> permettent de prélever ou retirer des champs. Sur les unions, Exclude<U, X> et Extract<U, X> filtrent. Sur les fonctions, Parameters<F>, ReturnType<F>, Awaited<P> exposent les éléments structurants. Ces utility types ne sont pas magiques : ils sont implémentés en pur TypeScript dans la bibliothèque standard, à partir de mapped types et conditional types.

interface Todo {
  id: number;
  title: string;
  done: boolean;
}

type TodoDraft  = Partial<Todo>;            // toutes optionnelles
type TodoView   = Pick<Todo, "id" | "title">;
type TodoUpdate = Omit<Todo, "id">;          // sans id
type TodoFrozen = Readonly<Todo>;

Le pas suivant consiste à écrire ses propres mapped types. La syntaxe { [K in keyof T]: ... } permet d’itérer sur les clés d’un type et de transformer leur valeur. Les modificateurs readonly et ? peuvent être ajoutés ou retirés avec + et -. Le key remapping apparu en 4.1 ([K in keyof T as NewKey]) permet de renommer les clés, ce qui est particulièrement puissant combiné aux template literal types pour générer des paires getter/setter ou des préfixes typés. Le tutoriel dédié couvre l’ensemble du sujet avec des exemples industriels : voir Utility et mapped types.

tsconfig.json et outillage

Le fichier tsconfig.json est le pilote du compilateur. Il indique où trouver les sources, où placer la sortie, quelles règles strictes appliquer, comment résoudre les modules. C’est aussi le point où un projet bien typé devient un projet vraiment sûr, parce que TypeScript expose une trentaine de drapeaux dont l’activation transforme la qualité des diagnostics. Le drapeau strict en active une famille complète : strictNullChecks, noImplicitAny, strictFunctionTypes, strictBindCallApply, strictPropertyInitialization, noImplicitThis, alwaysStrict, useUnknownInCatchVariables. À partir de TypeScript 6.0, ce drapeau est activé par défaut, ce qui change la trajectoire des nouveaux projets : on démarre directement avec un cadre exigeant.

Quelques drapeaux gagnent à être ajoutés au-delà de strict : noUncheckedIndexedAccess oblige à vérifier la présence d’une valeur avant de l’utiliser quand on indexe un dictionnaire, exactOptionalPropertyTypes distingue une propriété absente d’une propriété explicitement undefined, noImplicitOverride impose le mot-clé override lors d’une redéfinition de méthode, noFallthroughCasesInSwitch empêche les enchaînements involontaires entre case. Le tutoriel d’installation explique chaque option avec ses bénéfices et ses contraintes : voir Setup TSC et tsconfig.json.

Résolution de modules : nodenext, bundler, node20

La résolution de modules est le sujet qui pose le plus de problèmes aux nouveaux arrivants, en grande partie parce que l’écosystème JavaScript a accumulé plusieurs systèmes en parallèle. CommonJS, ESM, modules UMD, packages double-export… TypeScript expose plusieurs valeurs pour son option moduleResolution qui correspondent à des stratégies distinctes.

Pour un projet Node.js moderne qui utilise les modules ECMAScript, on choisit nodenext (ou désormais node20 stable depuis TypeScript 5.9, qui calque la version 20 LTS de Node sans suivre les évolutions futures). Pour un projet packagé par un bundler (Vite, esbuild, Webpack, Parcel), TypeScript 5.0 a introduit bundler qui reproduit le comportement de ces outils : extensions de fichiers facultatives, résolution simplifiée, support du champ exports du package.json. Pour une bibliothèque destinée à être consommée par d’autres, nodenext reste le choix le plus rigoureux car il oblige à respecter les contraintes de Node lui-même.

Côté syntaxe, le drapeau verbatimModuleSyntax (TypeScript 5.0) clarifie une zone qui était devenue floue : tout import type est effacé à la compilation, tout import sans type est conservé tel quel. Plus de surprises sur les imports qui disparaissaient ou survivaient selon que le compilateur estimait qu’ils étaient utilisés. Les bibliothèques modernes adoptent toutes ce drapeau parce qu’il rend le comportement prévisible et compatible avec les outils tiers (notamment swc et esbuild qui n’ont pas l’analyse de type pour décider).

Décorateurs stage 3

Pendant plus de cinq ans, les décorateurs de TypeScript étaient une fonctionnalité expérimentale activée par le drapeau experimentalDecorators. Ils étaient massivement utilisés dans des frameworks comme Angular ou NestJS, mais ils n’étaient pas alignés sur la proposition standard de TC39, ce qui empêchait leur adoption en JavaScript natif. TypeScript 5.0 a marqué le tournant : la proposition stage 3 du comité, désormais sur le point d’être intégrée à ECMAScript, est implémentée sans drapeau et avec un système de typage rigoureux.

Concrètement, un décorateur moderne est une fonction qui reçoit l’élément décoré et un objet context riche en métadonnées. Pour un décorateur de méthode, ce contexte expose le nom, l’accès, les indicateurs isStatic et isPrivate, et la méthode addInitializer qui permet de programmer un effet de bord lors de l’instanciation. Le retour de la fonction remplace l’élément décoré, ce qui rend la composition propre et prévisible.

function log(value: Function, context: ClassMethodDecoratorContext) {
  const name = String(context.name);
  return function (this: any, ...args: any[]) {
    console.log(`→ ${name}(${args.join(", ")})`);
    return value.apply(this, args);
  };
}

class Calculator {
  @log
  add(a: number, b: number) {
    return a + b;
  }
}

Deux limites à connaître : les décorateurs stage 3 ne peuvent pas décorer un paramètre de méthode, et ils ne sont pas compatibles avec emitDecoratorMetadata, le mécanisme qui exposait à l’exécution les types des paramètres via la bibliothèque reflect-metadata. Si vous travaillez avec un framework qui dépend encore de cette métadonnée (NestJS dans sa branche historique, TypeORM), vous resterez sur les décorateurs legacy pour ce projet, mais vous adopterez la version stage 3 sur les nouveaux. Le tutoriel dédié détaille chaque type de décorateur avec des cas concrets : voir Décorateurs.

Migrer un projet JavaScript vers TypeScript

La migration n’est pas un projet « tout-ou-rien ». TypeScript a été conçu pour permettre un passage incrémental, fichier par fichier, sans bloquer la livraison. La stratégie classique consiste à activer allowJs dans tsconfig.json, à laisser cohabiter .js et .ts, à renommer un fichier à la fois en commençant par les modules les plus stables et les moins exposés. Les annotations JSDoc permettent même de typer du JavaScript pur sans toucher à la syntaxe, ce qui est utile sur des fichiers très anciens qu’on ne veut pas remettre en cause.

Le drapeau checkJs active les vérifications de type sur les .js, et l’on peut désactiver localement avec // @ts-nocheck. À l’inverse, // @ts-expect-error permet de marquer une erreur connue qu’on tolère temporairement, avec la garantie que TypeScript signalera si l’erreur disparaît plus tard. Le tutoriel dédié décrit un plan de migration en cinq étapes appliqué à une base de code réelle : voir Migrer JS → TS.

Patterns industriels

Au-delà des constructions de base, plusieurs patterns reviennent dans toutes les bases de code professionnelles. Les discriminated unions modélisent des états applicatifs ou des messages : chaque variante porte un champ littéral qui sert d’étiquette, et l’on consomme l’union avec un switch exhaustif protégé par un never dans le cas par défaut, ce qui garantit qu’on ne pourra pas ajouter une variante sans mettre à jour le consommateur.

type Result<T> =
  | { kind: "ok"; value: T }
  | { kind: "err"; message: string };

function render<T>(r: Result<T>): string {
  switch (r.kind) {
    case "ok":  return `✓ ${JSON.stringify(r.value)}`;
    case "err": return `✗ ${r.message}`;
    default:
      const _exhaustive: never = r;
      return _exhaustive;
  }
}

Les branded types apportent un typage nominal partiel sur des primitives, pour distinguer par exemple un UserId d’un OrderId bien qu’ils soient tous deux des chaînes à l’exécution. On les construit avec une intersection sur un symbole unique, et l’on s’oblige à passer par un constructeur explicite. Ce pattern est précieux dès qu’on manipule plusieurs identifiants distincts sur des objets différents et qu’on veut éviter les inversions silencieuses.

Les type guards utilisateur (function isUser(x: unknown): x is User) permettent d’enseigner au compilateur des règles de narrowing qu’il ne peut pas déduire seul, généralement à la sortie d’un validateur de schéma. Combinés à Zod ou ArkType, ils ferment le contrat entre runtime et compile-time, ce qui est indispensable sur la frontière HTTP et la lecture des variables d’environnement. Enfin, le pattern satisfies introduit en 4.9 permet de vérifier qu’une valeur respecte un type sans pour autant l’élargir, ce qui préserve les littéraux et l’autocomplétion en aval.

Évolution 5.x → 6.0 → 7.0

Le rythme de TypeScript est trimestriel pour les versions mineures, et les versions majeures sont espacées de plusieurs années (4.0 en août 2020, 5.0 en mars 2023, 6.0 en mars 2026). Quelques jalons à retenir si l’on prend la pile en route en 2026. TypeScript 5.0 (mars 2023) a apporté les décorateurs stage 3, les const type parameters, l’option moduleResolution: "bundler", et verbatimModuleSyntax. TypeScript 5.4 a introduit NoInfer. TypeScript 5.9 a refondu tsc --init pour générer un tsconfig.json minimal aligné sur les bonnes pratiques actuelles, ajouté --module node20 et le support stage 3 de import defer.

TypeScript 6.0 sortie le 23 mars 2026 est une version charnière : strict activé par défaut, cible es2025, module esnext, noUncheckedSideEffectImports activé. L’option rootDir prend désormais comme valeur par défaut le dossier du tsconfig.json et types vaut désormais [], ce qui oblige à déclarer explicitement les paquets de types globaux (@types/node par exemple). La bibliothèque es2025 expose les nouveautés ECMAScript récentes : RegExp.escape, l’API Temporal pour les dates et durées, et les méthodes Map.getOrInsert et Map.getOrInsertComputed.

TypeScript 7.0 Beta, dévoilée le 21 avril 2026, est une réécriture complète du compilateur en Go, qui ouvre la voie à des temps de compilation environ dix fois plus courts grâce au code natif et au parallélisme partagé. Pour les développeurs, l’API et le langage restent identiques : il s’agit d’un changement d’implémentation, pas de spécification. Les premières expérimentations passent par le paquet @typescript/native-preview et le binaire tsgo.

Pièges fréquents

Quelques erreurs reviennent quasiment toujours chez les équipes qui démarrent. La première est de confondre les types et les valeurs : un type ne peut pas être utilisé à l’exécution, et inversement un objet ne peut pas être utilisé comme type sans passer par typeof. La seconde est l’abus de any, qui désactive le système de types et fait perdre tout le bénéfice du compilateur ; on préférera unknown, qui force à narrower avant d’utiliser. La troisième est de croire que TypeScript va valider les données à l’exécution, ce qui n’arrive jamais : aux frontières, on combine TypeScript avec un validateur de schéma.

D’autres pièges concernent la résolution de modules — ne pas oublier l’extension .js dans les imports en ESM, ne pas mélanger CommonJS et ESM dans le même paquet sans conditional exports, configurer correctement module et moduleResolution en cohérence avec son contexte d’exécution. Enfin, sur les classes, l’oubli du drapeau strictPropertyInitialization laisse passer des propriétés non initialisées qui produisent des undefined à l’exécution ; avec strict activé par défaut depuis 6.0, ce piège disparaît pour les nouveaux projets, mais reste à corriger sur les bases plus anciennes.

Les tutoriels de la série

Cette page de référence est volontairement panoramique. Chaque sujet abordé bénéficie d’un tutoriel pas-à-pas qui creuse l’implémentation, livre du code testé, et anticipe les erreurs courantes.

FAQ

TypeScript ralentit-il l’exécution de mon code ?
Non. À l’exécution, votre programme est du JavaScript : les annotations ont disparu lors de la compilation. La seule pénalité éventuelle est sur le temps de build, qui est négligeable sur les petites bases et que TypeScript 7.0 réduit drastiquement.

Faut-il valider les données à l’exécution si on a TypeScript ?
Oui, à chaque frontière du système : appels réseau, lectures de fichier, formulaires, variables d’environnement. TypeScript valide la cohérence interne du code ; il ne contrôle pas ce qui rentre depuis l’extérieur. On combine donc TypeScript avec Zod, Valibot ou ArkType pour fermer le contrat.

Quelle version installer aujourd’hui ?
La dernière version stable du moment : TypeScript 6.0.3 publiée le 16 avril 2026. Pour expérimenter le compilateur Go, on peut installer @typescript/native-preview en parallèle, sans remplacer l’installation principale.

Faut-il activer toutes les options strict ?
Pour un nouveau projet, oui. strict: true est activé par défaut depuis TypeScript 6.0. On ajoute généralement noUncheckedIndexedAccess, exactOptionalPropertyTypes et noImplicitOverride pour aller plus loin. Sur un projet existant, on active ces options progressivement, fichier par fichier.

Peut-on utiliser TypeScript sans bundler ?
Oui. Node.js exécute directement les .ts depuis la version 22 avec le drapeau initial --experimental-strip-types, et par défaut sans drapeau depuis Node 22.18 et Node 23.6. Deno et Bun exécutent du TypeScript sans aucune étape de build. Sur un bundler comme Vite ou esbuild, la compilation est intégrée et transparente.

Quelle différence entre type alias et interface ?
Une interface décrit une forme d’objet et tolère la fusion de déclaration. Un type alias décrit n’importe quel type, mais ne fusionne pas. Pour une forme d’objet exposée publiquement, on préfère interface. Pour une union, un tuple, ou une intersection complexe, on utilise type.

Les décorateurs sont-ils stables ?
Les décorateurs stage 3 le sont depuis TypeScript 5.0 et sont sur le point d’être intégrés à ECMAScript. Les décorateurs legacy (experimentalDecorators) restent supportés mais ne devraient plus être utilisés sur de nouveaux projets, sauf si un framework historique (NestJS classique, TypeORM) en dépend.

Ressources et références officielles

Service ITSkillsCenter

Site ou application web sur mesure

Conception Pro + Nom de domaine 1 an + Hébergement 1 an + Formation + Support 6 mois. Accès et code livrés. À partir de 350 000 FCFA.

Demander un devis
Publicité