Startseite / Artikel / Was die native TypeScript-Unterstützung in Node.js tatsächlich kann und nicht kann

Was die native TypeScript-Unterstützung in Node.js tatsächlich kann und nicht kann

Dieser Artikel erklärt, wie Node.js .ts-Dateien durch Entfernen der Typinformationen nativ ausführt, warum er die Typüberprüfung überspringt und wann dennoch ein echter Build-Schritt erforderlich ist.

1595 Wörter

Sie kennen den Ablauf inzwischen. Sie erstellen ein neues TypeScript-Projekt, schreiben Ihre erste .ts-Datei, versuchen, sie auszuführen, und erinnern sich sofort daran, dass es ein ganzes Einrichtungsritual gibt, das zuerst erledigt werden muss: ts-node oder tsx herunterladen, eine tsconfig.json konfigurieren, eventuell ein Build-Skript einrichten und herausfinden, ob Sie CommonJS oder ESM als Ziel verwenden. Keine dieser Aufgaben ist für sich genommen besonders schwierig. Es handelt sich einfach um Hindernisse, die sich ansammeln, bevor Sie überhaupt Logik für eine Anwendung schreiben können – und das passiert jedes Mal, wenn Sie etwas Neues starten.

In diesem Jahr ist bei einem erheblichen Teil der praktischen Node.js-Projekte dieses ganze Verfahren einfach verschwunden. Das Eingeben von node file.ts funktioniert einfach so – ohne Flags, ohne zusätzliche Abhängigkeiten, ohne Konfigurationsdateien. Die Änderung wurde leise eingeführt, ohne große Ankündigung, doch es handelt sich um eine dieser kleinen Entlastungen, auf die man sonst wöchentlich wiederholt stoßen würde – und das zusammen ergibt etwas, das sich wirklich lohnt, genauer zu untersuchen.

Was eigentlich vor sich geht

Der zugrunde liegende Mechanismus wird Typenentfernung genannt, und der Name ist erfrischend wörtlich gemeint: Node.js analysiert Ihre TypeScript-Quelldatei, entfernt die Typangaben und führt den verbleibenden reinen JavaScript-Code aus. Das ist das gesamte Konzept.

// 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`;
}

Die Deklaration der interface verschwindet völlig. Anmerkungen wie : User und : string werden gelöscht. Was übrig bleibt, ist gewöhnlicher, gültiger JavaScript-Code, den V8 genauso ausführt wie immer – es ist kein spezieller Laufzeitmechanismus beteiligt, keine Polyfills, und konzeptionell passiert zur Ausführungszeit nichts Neues.

Hinter den Kulissen läuft dieser Prozess über eine Bibliothek namens Amaro, die ein leichtgewichtiges Wrapper um @swc/wasm-typescript ist – eine WebAssembly-Kompilation des TypeScript-Parsers, den SWC in Rust entwickelt hat. Die Geschwindigkeit ergibt sich nicht aus irgendwelchen cleveren Optimierungen, sondern daraus, dass deutlich weniger Arbeit geleistet wird als bei einem vollständigen Compiler. Er löst keine Typen über mehrere Dateien auf, prüft nicht, ob Ihre Anmerkungen korrekt sind, und erzeugt keine Deklarationsdateien. Er analysiert einfach den Syntaxbaum, entfernt die für TypeScript spezifischen Teile und gibt JavaScript zurück. Genau dieser enge Aufgabenbereich ist der Grund für seine Geschwindigkeit.

Die Unterstützung für diese Funktion bei Node durchlief mehrere Phasen, bevor sie ihre heutige Form erreichte: Die experimentelle Unterstützung für das einfache Entfernen von Typinformationen wurde in v22.6.0 eingeführt, ein separates Flag zur Handhabung komplexerer Konstruktionen wie Enums erschien in v22.7.0, und die gesamte Funktion wurde standardmäßig in beiden Versionen v22.18.0 und v24.3.0 stabil – das bedeutet, dass für Code, der innerhalb der unterstützten Syntax liegt, überhaupt keine Flags benötigt werden. Bemerkenswerterweise entfernte Node später dieses spezielle Flag für Enums ganz und entschied sich stattdessen dafür, sich auf einen bewusst eng gefassten und vorhersehbaren Umfang zu konzentrieren, anstatt versuchen, die gesamte Sprache zu unterstützen.

Die ehrlichen Grenzen

Das ist der entscheidende Punkt, den man verstehen muss, wenn man sich auf diese Funktion verlassen will – und er sollte klar ausgedrückt werden: Das Entfernen von Typinformationen ist nicht dasselbe wie Typüberprüfung.

Das Entfernen einer Typangabe bestätigt nicht zunächst, dass sie korrekt ist – es löscht sie lediglich. Somit wird eine Datei, die einen echten Typfehler enthält – beispielsweise wenn an einer Stelle, an der eine Zahl erwartet wird, ein String übergeben wird – unter Typentfernung ohne jegliche Fehlermeldung ausgeführt, da zum Zeitpunkt der tatsächlichen Ausführung die Typinformationen, die das Problem angezeigt hätten, bereits verschwunden sind. Alle seriösen Quellen zu diesem Thema geben denselben Rat: Setzen Sie weiterhin tsc --noEmit als eigenständigen Schritt in Ihrem CI-Pipeline ein. Die Typentfernung ersetzt den Build-Schritt, nicht jedoch die Aufgabe des Compilers, Fehler tatsächlich aufzudecken.

Die bedeutendere Einschränkung betrifft genau die TypeScript-Syntax, die überhaupt für das Entfernen in Frage kommt. Node unterstützt nur sogenannte erasable Syntax – Sprachkonstrukte, die vollständig entfernt werden können, ohne dass sich das Verhalten des Codes bei der Ausführung ändert. Ein erheblicher Teil von TypeScript erfüllt diese Voraussetzung nicht, da er echtes Laufzeitverhalten hervorruft, das nicht einfach gelöscht werden kann:

// ❌ 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;

Jede dieser Konstruktionen führt zu einem fehlerhaften Abbruch anstelle eines stillschweigend fehlerhaften Builds – Node’s Entfernungsmechanismus ist absichtlich so konzipiert, dass er anhält und Fehlermeldungen ausgibt, anstatt zu raten, was man gemeint hat. Dekoratoren im Legacy-Stil, die über das alte Flag experimentalDecorators aktiviert werden, stoßen aus demselben Grund auf dieselbe Einschränkung. Die neueren, standardskonformen Dekoratoren von TC39 sind eine andere Geschichte: Sie sind so definiert, dass sie in normale JavaScript-Syntax kompiliert werden, sodass nichts Besonderes mehr entfernt werden muss, und sie laufen unter Node ohne Probleme.

Wie TypeScript sich angepasst hat

Anstatt den Entwicklern zu erlauben, diese Grenzen beim Ausführen des Codes Schritt für Schritt in einzelnen Dateien zu entdecken, handelte das TypeScript-Team schnell und machte die Regeln explizit. TypeScript 5.8 brachte eine neue Compiler-Flagge ein, --erasableSyntaxOnly, die dazu führt, dass tsc selbst während der Kompilierung alle oben beschriebenen nicht lösbaren Muster ablehnt. Dadurch ändert sich die Frage „Wird Node diesen Code tatsächlich ausführen?“ von etwas, das man zur Laufzeit auf die harte Tour herausfindet, zu einer Regel, die bereits im Voraus durchgesetzt werden kann – als explizite Einschränkung für die gesamte Codebasis.

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

Auch wenn Sie derzeit noch keine Pläne haben, Ihren Build-Prozess zu entfernen, lohnt es sich, diese Option einzuschalten – schließlich liefert sie Ihnen eine eindeutige, automatisierte Antwort darauf, ob Ihr Code geeignet ist – anstatt dies erst nach und nach herauszufinden, wenn Probleme auftreten.

Die Menge an Migrationsaufwand, die dadurch aufgedeckt wird, variiert stark je nach Ausgangslage. Ein neu erstellter Backend-Dienst oder eine Kommandozeilen-Tool kann in der Regel sofort erasableSyntaxOnly aktivieren, wobei kaum oder gar nichts korrigiert werden muss. Eine Codebasis, die stark auf enum-Deklarationen angewiesen ist oder auf einem Framework basiert, das herkömmliche Dekoratoren voraussetzt – ältere Konfigurationen von NestJS oder TypeORM sind die häufigsten Beispiele – erfordert tatsächlich eine Umgestaltung oder einen bewussten Entschluss, anstelle einer einmaligen Migration weiterhin einen traditionellen Build-Prozess zu verwenden. Die zuverlässigste Methode, den Aufwand vor einer endgültigen Entscheidung einzuschätzen, besteht darin, erasableSyntaxOnly zu aktivieren, einmal tsc --noEmit auszuführen und einfach zu prüfen, wie viele Fehler auftauchen. Diese einzige Ausführung gibt bereits vor der Anpassung jeglicher Laufzeitkonfiguration einen Einblick in den tatsächlichen Umfang des Problems.

Praktische Anleitung

In Teams, die bereits diese Umstellung durchlaufen haben, hat sich ein recht konsistentes Entscheidungsframework herausgebildet.

Lassen Sie den Build-Schritt für Backend-Dienste, Kommandozeilenwerkzeuge, interne Hilfsprogramme sowie eigenständige Skripte weg – also alles, was direkt unter Node läuft und nicht als Paket veröffentlicht wird, damit andere es nutzen können. Genau für diesen Fall wurde stripping entwickelt, und es wird häufig berichtet, dass Dienste, die auf Express oder Fastify basieren, sofort damit funktionieren, ohne dass Codeänderungen erforderlich sind.

Behalten Sie einen Build-Schritt für alles, was im Browser ausgeführt wird, da Browser .ts-Dateien überhaupt nicht ausführen können – unabhängig davon, welche Funktionen Node auf dem Server unterstützt, benötigen Sie weiterhin einen Bundler. Behalten Sie außerdem einen Build-Schritt für jedes npm-Paket, das Sie veröffentlichen, denn die Nutzer benötigen kompilierten JavaScript sowie .d.ts-Deklarationsdateien, und es gibt keine Garantie dafür, dass ihre Node-Version überhaupt Typentfernung unterstützt. Ebenso sollten Sie einen Build-Schritt für Codebasen beibehalten, die noch auf veralteten Dekoratoren oder intensiver Verwendung von enum-Typen angewiesen sind und noch nicht konvertiert wurden.

Egal für welchen Weg Sie sich entscheiden, führen Sie in CI weiterhin tsc --noEmit aus. Das Weglassen des Build-Schritts entfernt lediglich den Kompilierungsprozess – er war niemals dazu gedacht, die Typüberprüfung zu ersetzen, und seine Verwendung als Ersatz für tsc ist der eigentliche Grund, warum diese Änderung heimlich Ihre Sicherheit gefährden kann.

Die eigentliche Erkenntnis

Was diesen Wandel interessant macht, ist nicht in erster Linie der Geschwindigkeitsvorteil – auch wenn der schnellere Feedbackzyklus tatsächlich einen unmittelbaren Nutzen bietet. Es handelt sich dabei um ein Signal darüber, wohin TypeScript konzeptionell steuert. Über weite Teile seiner Geschichte wurde TypeScript als eine Sprache beschrieben, die in JavaScript kompiliert wird – eine eigenständige Sprache, die vor der Ausführung übersetzt werden muss. Die im Node-Umfeld stattfindende Typentfernung deutet darauf hin, dass TypeScript zunehmend eher wie eine Variante von JavaScript behandelt wird, die ein Laufzeitumfeld einfach so lesen kann – zumindest für den breiten, alltäglichen Teil der Sprache, den die meisten Entwickler tatsächlich verwenden. Das ist jedoch nicht die gesamte Sprache, und das war auch nie der Fall – Enums sowie herkömmliche Dekoratoren haben weiterhin reale Anwendungsfälle und verschwinden nicht. Doch für all den Code, der sie nicht benötigt, ist der Schritt, der früher zwischen dem Schreiben von TypeScript und seiner Ausführung lag, nicht mehr zwingend erforderlich.

Das ist ein bedeutenderer Wandel, als die bescheidene Aufmerksamkeit, die ihm zuteilwurde, vermuten ließe.

Verwandte Artikel