Accueil / Articles / Ce que le support natif de TypeScript dans Node.js permet réellement et ne permet pas

Ce que le support natif de TypeScript dans Node.js permet réellement et ne permet pas

Cet article explique comment Node.js exécute les fichiers .ts de manière native grâce au suppression des types, pourquoi il omet la vérification des types, et quand vous avez encore besoin d’une véritable étape de compilation.

1595 mots

.ts, essayez de l’exécuter, puis vous vous souvenez immédiatement qu’il y a toute une série d’étapes de configuration à effectuer en premier : installer ts-node ou tsx, configurer un tsconfig.json, éventuellement mettre en place un script de compilation, et déterminer si vous visez CommonJS ou ESM. Aucune de ces étapes n’est particulièrement difficile en soi. Il s’agit simplement de frictions qui s’accumulent avant même d’avoir écrit la moindre logique d’application, et cela se produit à chaque fois que vous commencez un nouveau projet.

Cette année, pour une grande partie des projets Node.js en environnement réel, tout ce rituel a simplement disparu. Il suffit de taper node file.ts pour que cela fonctionne. Pas de paramètres, pas de dépendances supplémentaires, pas de fichiers de configuration. Ce changement a été mis en œuvre discrètement, sans aucune annonce majeure, mais il s’agit du genre d’élimination de petits obstacles que l’on rencontre fréquemment au cours de la semaine — et qui, accumulés, en font quelque chose vraiment digne d’être exploré.

Que se passe-t-il réellement ?

Le mécanisme sous-jacent s’appelle type stripping, et son nom est étonnamment littéral : Node.js analyse votre code TypeScript, supprime les annotations de type, puis exécute le JavaScript pur qui reste. C’est tout l’idée.

// Before: what you write
interface User {
  name: string;
  age: number;
}
function describeUser(user: User): string {
  return `${user.name} is ${user.age} years old`;
}
// After: what Node.js actually executes, post-stripping
// (whitespace preserved, so line numbers stay accurate for debugging)
function describeUser(user) {
  return `${user.name} is ${user.age} years old`;
}

La déclaration de l’interface disparaît complètement. Les annotations telles que : User et : string sont effacées. Ce qui reste, c’est du JavaScript ordinaire et valide que V8 exécute comme d’habitude — il n’y a pas de mécanisme d’exécution spécial, aucun polyfill, et rien de nouveau sur le plan conceptuel au moment de l’exécution.

Dans l’ombre, ce processus s’appuie sur une bibliothèque nommée Amaro, qui constitue un enveloppement léger autour de @swc/wasm-typescript, une compilation en WebAssembly du parseur TypeScript que SWC a développé en Rust. La vitesse obtenue ne provient pas d’optimisations ingénieuses, mais plutôt du fait que ce système effectue beaucoup moins de travail qu’un compilateur complet. Il ne résout pas les types entre plusieurs fichiers, ne vérifie pas la correction de vos annotations et ne génère pas de fichiers de déclaration. Il se contente d’analyser l’arbre syntaxique, d’éliminer les éléments propres uniquement à TypeScript et de restituer du JavaScript. Cet champ d’action restreint est précisément la raison de sa rapidité.

Le support de Node pour cette fonctionnalité a traversé plusieurs phases avant d’atteindre sa forme actuelle : un support expérimental pour le suppression simple des types est apparu dans v22.6.0, un paramètre distinct pour gérer des constructions plus complexes comme les enums est apparu dans v22.7.0, et l’ensemble de cette fonctionnalité est devenu stable par défaut tant dans v22.18.0 que dans v24.3.0 — ce qui signifie qu’aucun paramètre n’est nécessaire pour le code respectant la syntaxe prise en charge. Il convient de noter que Node a par la suite supprimé complètement ce paramètre spécifique aux enums, préférant s’engager dans un champ d’application délibérément restreint et prévisible plutôt que de tenter de prendre en charge tout le langage.

Les limites réelles

C’est ici le point crucial à comprendre si vous comptez vous fier à cette fonctionnalité, et il convient de le formuler clairement : la suppression des types n’est pas la même chose que la vérification des types.

Supprimer une annotation de type ne confirme pas d’abord qu’elle est correcte — elle la supprime simplement. Ainsi, un fichier contenant une véritable erreur de type, par exemple en passant une chaîne là où un nombre était attendu, fonctionnera sans aucun problème sous l’effet du suppression des types, car au moment où le code s’exécute réellement, les informations de type qui auraient permis d’identifier le problème ont déjà disparu. Tous les documents sérieux sur ce sujet donnent le même conseil : continuez à exécuter tsc --noEmit en tant qu’étape distincte dans votre pipeline CI. Le suppression des types remplace l’étape de compilation, mais pas la fonction du compilateur qui consiste à détecter les erreurs réellement.

La restriction la plus importante concerne précisément les syntaxes TypeScript qui peuvent être supprimées. Node ne prend en charge que ce qu’on appelle la syntaxe érasable — des constructions linguistiques qui peuvent être entièrement retirées sans modifier le comportement du code lors de son exécution. Une partie significative de TypeScript ne répond pas à ce critère, car elle génère un véritable comportement en temps de exécution qui ne peut pas simplement être supprimé :

// ❌ Fails under type stripping — enums generate a real runtime object
enum Direction {
  Up,
  Down,
  Left,
  Right,
}
// ❌ Fails - parameter properties generate constructor assignment code
class Point {
  constructor(public x: number, public y: number) {}
}
// ❌ Fails - this is a CommonJS-style module alias, not an erasable type
import fs = require('fs');
// ❌ Fails - angle-bracket type assertions look like real syntax to strip,
// but the parser can't tell it apart from JSX safely
const num = <number>someValue;

Chacune de ces constructions provoque une erreur fatale plutôt qu’une compilation incorrecte silencieuse — le mécanisme de suppression de Node est conçu exprès pour s’arrêter et signaler un problème, au lieu de deviner ce que vous vouliez dire. Les décorateurs de style ancien, activés grâce à l’ancienne option experimentalDecorators, rencontrent le même problème pour la même raison. Les décorateurs plus récents, conformes aux normes établies par TC39, sont différents : ils sont définis de manière à se compiler en syntaxe JavaScript ordinaire, il n’y a donc rien de spécial à supprimer, et ils fonctionnent sans aucun problème sous Node.

Comment TypeScript s’est adapté

Au lieu de laisser les développeurs découvrir ces limites un fichier à la fois en exécutant du code, l’équipe de TypeScript a rapidement rendu ces règles explicites. La version 5.8 de TypeScript a introduit un nouveau paramètre de compilateur, --erasableSyntaxOnly, qui fait en sorte que tsc rejette automatiquement tous les schémas non érasables mentionnés ci-dessus pendant la compilation. Cela transforme la question « Node exécutera-t-il réellement ce code ? » en une règle que l’on peut appliquer dès le départ, en tant que contrainte explicite pour l’ensemble du codebase, au lieu de devoir le découvrir à la force des choses en temps de exécution.

// tsconfig.json
{
  "compilerOptions": {
    "erasableSyntaxOnly": true,
    "verbatimModuleSyntax": true  // pairs well with this —
                                   // keeps type-only imports explicit
  }
}

Il est utile d’activer cette option, même si vous n’avez pas l’intention de supprimer votre étape de compilation pour le moment, simplement parce qu’elle vous fournit une réponse définitive et automatisée sur la conformité de votre code — au lieu d’apprendre progressivement les problèmes au fur et à mesure qu’ils surviennent.

La quantité de travail de migration nécessaire varie considérablement en fonction du point de départ. Un service backend ou un outil en ligne de commande nouvellement créé peut généralement activer erasableSyntaxOnly immédiatement, avec peu ou pas de corrections à apporter. En revanche, une base de code qui dépend fortement des déclarations enum, ou celle construite sur un framework prévoyant l’utilisation de décorateurs anciens — comme les configurations plus anciennes de NestJS ou TypeORM — exige un travail de réécriture important, ou bien une décision consciente de conserver un pipeline de construction traditionnel plutôt que de migrer tout en une seule fois. La manière la plus fiable d’évaluer l’ampleur du travail avant de s’engager est d’activer erasableSyntaxOnly, d’exécuter tsc --noEmit une fois, et de compter le nombre d’erreurs générées. Cette seule exécution vous indiquera l’étendue réelle du problème avant même d’intervenir sur les configurations en temps de exécution.

Guides pratiques

Dans les équipes ayant déjà effectué cette transition, un cadre de décision assez cohérent s’est dégagé.

Omettez l’étape de compilation pour les services backend, les outils en ligne de commande, les utilitaires internes et les scripts autonomes — tout ce qui s’exécute directement sous Node sans être publié en tant que package destiné à d’autres utilisateurs. C’est précisément le cas pour lequel la fonction de suppression des types a été conçue, et il est courant de constater que les services basés sur Express ou Fastify fonctionnent immédiatement avec elle, sans aucune modification de code nécessaire.

Prévoyez une étape de compilation pour tout ce qui s’exécute dans un navigateur, car les navigateurs ne peuvent absolument pas exécuter des fichiers .ts — vous aurez toujours besoin d’un outil de bundling, quel que soit le support de Node sur le serveur. Prévoyez également une étape de compilation pour tout paquet npm que vous publiez, car les personnes qui le installent ont besoin de JavaScript compilé ainsi que de fichiers de déclaration .d.ts, et vous n’avez aucune garantie que leur version de Node prenne en charge le suppression des types. Enfin, prévoyez une étape de compilation pour tout codebase qui dépend encore de décorateurs obsolètes ou d’un usage intensif d’enum qui n’a pas encore été converti.

Quelle que soit la voie que vous choisissez, continuez à exécuter tsc --noEmit dans le CI. Supprimer l’étape de compilation ne fait que retirer l’étape de compilation elle-même — elle n’a jamais été conçue pour éliminer la vérification des types, et la considérer comme un remplacement de tsc est la seule façon réelle pour que ce changement vous coûte discrètement en sécurité.

La leçon principale

Ce qui rend ce changement intéressant n’est pas principalement l’amélioration de la vitesse, même si un cycle de retour plus rapide représente effectivement un avantage immédiat. Il s’agit plutôt d’un signe indiquant la direction conceptuelle que prend TypeScript. Tout au long de son histoire, TypeScript a été décrit comme un langage qui se compile en JavaScript — un langage distinct, traduit avant d’être exécuté. Le retrait automatique des types dans Node suggère discrètement que TypeScript tend à être considéré davantage comme une variante de JavaScript que le moteur d’exécution peut lire directement, du moins pour la grande partie quotidienne du langage utilisée par la plupart des développeurs. Ce n’est pas l’ensemble du langage, et ce ne le sera jamais — les enums et les décorateurs anciens ont encore des cas d’usage réels et ne disparaîtront pas. Mais pour tout le code qui n’en a pas besoin, l’étape qui existait auparavant entre l’écriture de TypeScript et son exécution n’est plus obligatoire, un

C’est un changement bien plus significatif que ce à quoi pourrait le laisser penser l’attention modeste qu’il a reçue.

Lectures complémentaires