zurück zum Artikel

Webentwicklung: Der neue TypeScript-Compiler in Go

Sebastian Springer
TypeScript und Go

(Bild: erzeugt mit KI durch iX)

Das TypeScript-Team portiert den Kern der Programmiersprache nach Go. Das macht die Sprache schneller und verbessert die Developer Experience.

TypeScript wurde einst aus der Not geboren, um die größten Schwächen von JavaScript auszugleichen. Seit Jahren ist es nun ein nahezu unverzichtbares Werkzeug in der Webentwicklung, das in seiner Entwicklung auf Feature-Ebene immer einen leichten Vorsprung zu JavaScript besitzt.

Microsoft hat kürzlich angekündigt, die Basis von TypeScript vollständig zu überarbeiten – also den Compiler, der TypeScript in JavaScript übersetzt, und weitere Komponenten, die für die Interaktion mit Tools und Entwicklungsumgebungen verantwortlich sind. Dabei hat Microsoft eine für viele überraschende Wahl getroffen. Die meisten haben damit gerechnet, dass, wenn jemand diesen Schritt geht, Rust als Programmiersprache zum Einsatz kommt, denn es gibt bereits populäre Rust-basierte Werkzeuge im JavaScript-Umfeld, etwa den Bundler Rspack.

Sebastian Springer
Sebastian Springer

Sebastian Springer weckt als Dozent für JavaScript, Sprecher auf zahlreichen Konferenzen und Autor die Begeisterung für professionelle Entwicklung mit JavaScript.

Doch das TypeScript-Team mit Anders Hejlsberg als Lead Architect hat sich stattdessen für eine Reimplementierung in der Sprache Go entschieden. Das Ziel der nativen Implementierung in Go, die kompiliert wird – im Gegensatz zur Just-in-Time-Umwandlung der Skriptsprache TypeScript in Bytecode – ist eine Verbesserung der Ausführungszeit der TypeScript-Werkzeuge sowie eine spürbare Reduzierung des Arbeitsspeicherverbrauchs. Laut aktueller Benchmarks erzielt das neue TypeScript eine zehnfach schnellere Zeit beim Build verschiedener Projekte.

Dieser Artikel widmet sich den Fragen, warum diese Verbesserung so wichtig ist, wo die aktuellen Schwächen von TypeScript liegen und wie die Entwicklung des neuen Compilers, der unter dem Codenamen Corsa und unter der Version TypeScript 7 geführt wird, vonstatten geht.

Im Jahr 2012 veröffentlichte Microsoft eine erste Version von TypeScript nach zwei Jahren Entwicklungszeit. Seit 2014 wird die Programmiersprache auf GitHub öffentlich einsehbar weiterentwickelt und unterliegt als Open-Source-Projekt der Apache-2.0-Lizenz. Mittlerweise hat TypeScript die Version 5.8 erreicht [1].

Im Kern ist TypeScript eine Programmiersprache, die JavaScript um Typsicherheit zur Kompilierungszeit ergänzt – mit den Zielen, besser größere Applikationen entwickeln zu können und eine Grundlage für bessere Entwicklungswerkzeuge zu schaffen.

Es gibt eine ganze Reihe von Gründen, die für den Einsatz von TypeScript statt JavaScript sowohl in Frontend- als auch in Backend-Projekten im Web sprechen:

Was bemerkenswert ist: TypeScript ist (noch) vollständig in TypeScript programmiert. Der Kern der Programmiersprache wird auch in Version 6 noch in TypeScript weitergeführt. Parallel dazu arbeitet das Team an Version 7, die dann die Go-Variante sein und die TypeScript-Implementierung ablösen wird, sobald die beiden Projekte eine Feature-Gleichheit erreicht haben. Der aktuelle Zeitplan des Entwicklungsteams sieht für Mitte des Jahres 2025 eine vollwertige Version des Kommandozeilen-Compilers vor, der in der Lage ist, die Typüberprüfung und Übersetzung von Quellcode durchzuführen.

Momentan funktioniert der TypeScript-Go-Compiler bereits nahezu vollständig für reinen TypeScript-Code. Was noch fehlt, ist die Unterstützung von JavaScript, JSDoc und JSX. Ende des Jahres 2025 wird eine Feature-vollständige Version erwartet, die Projekte bauen kann. Außerdem soll mit dem Language Service eines der wichtigsten Entwicklungsfeatures verfügbar sein.

Warum aber ist ein so drastischer Schritt wie ein Rewrite in einer anderen Programmiersprache überhaupt erforderlich? TypeScript spielt seine Vorteile vor allem in größeren Applikationen aus. Mit wachsender Codebasis wächst jedoch auch der Ressourcenbedarf von TypeScript beim Type Checking und beim Build. Das führt zum einen dazu, dass das Öffnen eines Projekts in der Entwicklungsumgebung lange dauert, da der Code hier durch den Language Service initial verarbeitet wird. Und auch der Build einer größeren Applikation kann sehr zeitintensiv werden. Zur reinen Laufzeit kommt noch ein weiteres, sogar noch größeres Problem durch den hohen Bedarf an Arbeitsspeicher. Dieser kann zu Speicherproblemen und im schlimmsten Fall zu Abstürzen durch Out-of-Memory-Fehlern führen.

JavaScript-Konferenz von Heise: enterJS 2025
Enterprise-JavaScript-Konferenz enterJS 2025, 7. und 8. Mai in Mannheim

(Bild: WD Ashari/Shutterstock.com)

Die enterJS 2025 [2] findet am 7. und 8. Mai in Mannheim statt. Die Konferenz bietet einen umfassenden Blick auf die JavaScript-gestützte Enterprise-Welt. Der Fokus liegt nicht nur auf den Programmiersprachen JavaScript und TypeScript selbst, sondern auch auf Frameworks und Tools, Accessibility, Praxisberichten, UI/UX und Security.

Highlights aus dem Programm:

Tickets sind im Online-Shop [6] erhältlich.

Der Rewrite von TypeScript in Go ist eigentlich kein Rewrite, sondern eine 1:1-Portierung des bestehenden Quellcodes. Damit will das Entwicklungsteam sicherstellen, dass die bestehenden Features in gleicher Form auch in der neuen Version verfügbar sind und sich vor allem auch bei Fehlermeldungen und Edge Cases gleich verhalten. Die JavaScript-Engine ist für den Einsatz im Browser gedacht und eignet sich weniger für CPU-intensive Aufgaben, wie sie im Zuge von TypeScripts Compile-Prozess anfallen. Außerdem sind eine echte Parallelisierbarkeit und der Einsatz von Shared Memory in JavaScript nur sehr eingeschränkt möglich.

Go bietet im Gegensatz zur Kombination aus TypeScript und JavaScript eine ganze Reihe von Vorteilen. So ist Go eine statisch kompilierte Sprache, deren Quellcode direkt in Maschinencode übersetzt wird. JavaScript ist dagegen eine Just-in-Time-Sprache, deren Quellcode die JavaScript-Engine erst bei der Ausführung in Bytecode umwandelt und anschließend interpretiert. Go verfügt über eine automatische Speicherverwaltung mit einem Garbage Collector, erlaubt Parallelisierung von Aufgaben und besitzt effiziente Möglichkeiten zum Datenaustausch wie Channels und Shared Memory.

Die beeindruckende zehnmal schnellere Performance der TypeScript-Go-Implementierung hat laut Anders Hejlsberg zwei Gründe. Zum einen ist der native Go-Code deutlich schneller im Vergleich zum JavaScript-Code der ursprünglichen Implementierung und zum anderen ist der Compiler mit der neuen Architektur in der Lage, bestimmte Aufgaben zu parallelisieren.

Dafür greift der neue Compiler in die Phasen des Verarbeitungsprozesses des Compilers ein. Die Verarbeitung einzelner Dateien durch Parser, Binder und Emitter kann der Compiler problemlos parallelisieren. Die Verantwortlichkeiten dieser drei Werkzeuge sind:

Zwischen dem Binder und dem Emitter steht der Type Checker, der die Typkonsistenz des Quellcodes überprüft. Sind Typannotationen fehlerhaft oder werden Variablen oder Funktionen mit den falschen Typen genutzt, gibt er eine Fehlermeldung aus. Dieses Element des Compilers lässt sich nicht wie die übrigen Bestandteile parallelisieren. Stattdessen geht das Entwicklungsteam hier einen anderen Weg und verarbeitet den Quellcode mit mehreren Instanzen des Checkers. Jede Instanz ist dabei für einen Teil des Codes verantwortlich. Das führt zwar zu Überschneidungen bei der Überprüfung mit etwas Memory Overhead, beschleunigt den Prozess aber insgesamt.

Der erste Teil von TypeScript, den die Entwickler nach Go portiert haben, ist der Kommandozeilen-Compiler tsc, der den TypeScript-Code einer Applikation überprüft und in JavaScript übersetzt. Dieses Werkzeug kommt vor allem während des Build-Prozesses einer Applikation zum Einsatz.

TypeScript besteht jedoch nicht nur aus dem tsc. Eine der wichtigsten weiteren Komponenten ist der Language Service. Dieser Teil ist für die Komfort-Features in modernen Entwicklungsumgebungen mitverantwortlich. Der Language Service ist ein langlebiger Hintergrundprozess, der zu Beginn den gesamten Quellcode einliest und verarbeitet. Auf Basis der gesammelten Informationen stellt er die Grundlage für die Tooltips beim Hovern mit der Maus über Codestrukturen zur Verfügung, hilft bei der Navigation im Code und sorgt für das Highlighting von Fehlern im Code.

Für eine gute Developer Experience ist der Language Service von herausragender Bedeutung. Ein langsamer Verarbeitungsprozess schlägt sich in einem langwierigen Öffnen von Projekten in der IDE nieder, verzögerte Antwortzeiten schränken die Features der IDE ein und ein übermäßiger Speicherverbrauch verlangsamt die IDE und das Gesamtsystem.

In der Prioritätsreihenfolge bei der Portierung folgt der Language Service direkt nach dem Kommandozeilen-Compiler. Ein Prototyp des Language Service ist bereits jetzt für Visual Studio Code verfügbar. Zu den ersten umgesetzten Features gehören die Anzeige von Fehlern, die Unterstützung von Tooltips und das Springen zur Definition einer Variablen, Funktion oder eines Typs.

Neben dem Kommandozeilen-Compiler und dem Language Service arbeiten die Entwickler an einer Schnittstelle, über die andere Prozesse mittels Interprocess Communication mit dem Language Service kommunizieren und die Typinformationen abfragen können. Bei dieser Schnittstelle ist noch einiges an Konzeptarbeit offen. Fest steht, dass sie das LSP, das Language-Server-Protokoll, unterstützen soll.

Das Team entwickelt die Go-Variante von TypeScript auf GitHub in einem eigenständigen Repository [7] mit dem Namen typescript-go. Es ist öffentlich verfügbar und ermöglicht es, die frühe Entwicklungsversion zu testen. Die aktuelle Version ist ausdrücklich nicht für den Produktivbetrieb geeignet und auch noch nicht vollständig Feature-kompatibel mit der ursprünglichen TypeScript-Version. Auf diese Weise erhofft sich das Entwicklungsteam frühes Feedback von der Community und bezieht auch die Maintainer von Werkzeugen, Bibliotheken und Frameworks zu einem sehr frühen Zeitpunkt mit ein.

Zum Zeitpunkt des Schreibens des Artikels muss typescript-go selbst gebaut werden. Dafür muss das Zielsystem einige Voraussetzungen erfüllen:

Als Basis für die folgenden Tests dient das offizielle Docker-Image von Go. Der Vorteil ist, dass mit diesem Setup keine zusätzliche Software auf dem Zielsystem erforderlich ist und die Experimente reproduzierbar sind. Einzige Voraussetzung hierfür ist eine Container-Runtime wie beispielsweise Docker.

Die Befehle aus Listing 1 sorgen dafür, dass eine gebaute und ausführbare Version von typescript-go im Container vorhanden ist. Die Version von typescript-go ist in der Lage, TypeScript-Projekte zu bauen, um sie anschließend auszuführen.

docker run -it --rm golang:bookworm
apt update && apt install -y nodejs npm
git clone --recurse-submodules https://github.com/microsoft/typescript-go.git
npm ci
npx hereby build

Listing 1: Bauen von ts-go in einem Container

Die Befehlsfolge startet einen Container auf Basis des Go-Images und installiert Node.js. Anschließend klont der git-Befehl das typescript-go-Repository mit allen zugehörigen Submodulen. npm ci steht für clean install und sorgt dafür, dass alle npm-Abhängigkeiten installiert werden. Der Aufruf von npx hereby build startet den Build-Task, der in der hereby-Konfiguration definiert ist, und baut damit typescript-go.

Nach dieser Prozedur ist im built/local-Verzeichnis das Kommando tsgo verfügbar. Es verhält sich standardmäßig wie der TypeScript-Compiler auf der Kommandozeile mit der Option --diagnostics. Es kompiliert den TypeScript-Quellcode anhand einer vorhandenen TypeScript-Konfiguration und gibt zusätzliche Diagnoseinformationen wie die Laufzeit der einzelnen Phasen oder den Speicherverbrauch aus.

Als ersten Testlauf soll der typescript-go-Compiler den Code aus Listing 2 übersetzen.

function add(a: number, b: number): number {
  return a + b;
}

const result = add(1, 2);

console.log(result);

Listing 2: TypeScript-Quellcode für die Übersetzung

Voraussetzung für einen funktionierenden Build ist das Vorhandensein einer passenden tsconfig.json-Datei. Ander als der originale tsc ist die Go-Variante noch nicht in der Lage, eine solche Konfiguration zu generieren. Ein Aufruf von npx tsc --init nutzt die TypeScript-Variante, um die Voraussetzung zu erfüllen. Ein anschließender Aufruf von built/local/tsgo übersetzt den Code des Beispiels in JavaScript und macht ihn ausführbar. Schon bei diesem einfachen Beispiel zeigt sich ein deutlicher Unterschied zwischen der TypeScript- und der Go-Variante des Compilers.

Tabelle 1 stellt die Ergebnisse gegenüber. Sie zeigt für die ursprüngliche TypeScript-Variante sowie für tsgo im Single-Threaded-Modus und mit Parallelisierung, wie viel Speicher der Compile-Vorgang benötigte. Zudem listet sie die Zeit auf, die die einzelnen Phasen in Anspruch genommen haben, sowie die Gesamtzeit des Compile-Vorgangs.

  tsc tsgo -singleThreaded tsgo
Verbrauchter Speicher 54,411K 18,536K 19,494K
Parse-Zeit 0,10s 0,025s 0,019s
Bind-Zeit 0,06s 0,008s 0,006s
Check-Zeit 0,01s 0,001s 0,002s
Emit-Zeit 0,00s 0,00s 0,00s
Gesamtzeit 0,17s 0,034s 0,027s

Tabelle 1: Compile-Prozess einer einfachen Datei

Dieses Bild setzt sich fort, nicht nur bei kleinen TypeScript-Applikationen wie einer einfachen Nest.js-Applikation, die mit der Nest-CLI initialisiert wurde, sondern auch bei umfangreichen Codebasen wie Visual Studio Code, das in TypeScript geschrieben ist.

Tabelle 2 enthält die Ergebnisse des Compile-Vorgangs von date-fns, einer TypeScript-Bibliothek für Datums- und Zeit-Handling mit insgesamt 334.000 Zeilen. Für die TypeScript-, die Single-Threaded- und die Multi-Threaded-Variante schlüsselt die Tabelle den Speicherverbrauch sowie die Laufzeiten auf.

  tsc tsgo -singleThreaded tsgo
Verbrauchter Speicher 526,871K 270,145K 291,984K
Parse-Zeit 0,79s 0,246s 0,164s
Bind-Zeit 0,24s 0,057s 0,048s
Check-Zeit 1,35s 0,317s 0,216s
Emit-Zeit 0,36s 0,184s 0,096s
Gesamtzeit 2,75s 0,804s 0,523s

Tabelle 2: Compile-Prozess von date-fns

Die Optimierungen des neuen Compilers wirken sich also sowohl auf die Laufzeit als auch auf den Speicherverbrauch aus. Das Kommando tsgo -singleThreaded schaltet die Parallelisierung der Aufgaben im Compiler ab, sodass nur die Verbesserungen durch den nativen Go-Code sichtbar werden. Ohne diese Option parallelisiert der Compiler die Verarbeitung des Quellcodes und erreicht dadurch noch bessere Verarbeitungszeiten. Dies geht jedoch auf Kosten eines leicht erhöhten Speicherverbrauchs.

Mit der Portierung des TypeScript-Compilers von TypeScript auf Go adressiert das TypeScript-Entwicklungsteam einige Probleme, die gerade bei einer großen Codebasis in einem Projekt entstehen können. Die ursprüngliche Wahl von TypeScript bot eine Reihe von Vorteilen bei der Verarbeitung von TypeScript-Code, was jedoch zu Nachteilen bei der Skalierbarkeit und Performance führte. Die Entscheidung für Go wurde aufgrund der besonderen Eignung der Programmiersprache hinsichtlich komplexer Datenstrukturen, guter Speicherverwaltung und Parallelisierbarkeit getroffen.

Mit der Portierung und der Arbeit an einer verbesserten Schnittstelle zu anderen Prozessen eröffnen sich mit zukünftigen TypeScript-Versionen auch neue Möglichkeiten der KI-Integration, da KI-gestützte Werkzeuge über diese API besser mit den Typinformationen des Quellcodes interagieren können. Anders Hejlsberg skizziert im Interview [8] beispielsweise die Möglichkeit beim Refactoring, dass ein Large Language Model (LLM) während des Refactoring-Prozesses gleich einen passenden Funktionsnamen vorschlagen könnte.

Zwar wird bis zum finalen Release der Go-Portierung von TypeScript noch etwas Zeit vergehen, doch der Code im typescript-go-Repository soll kontinuierlich weiterentwickelt und um weitere Features ergänzt werden und sich so der Fertigstellung immer weiter annähern.

(mai [9])


URL dieses Artikels:
https://www.heise.de/-10355589

Links in diesem Artikel:
[1] https://www.heise.de/news/TypeScript-5-8-verbessert-die-Interoperabilitaet-zwischen-CommonJS-und-ECMAScript-10301548.html
[2] https://enterjs.de/index.php?wt_mc=intern.academy.dpunkt.konf_dpunkt_vo_enterJS.empfehlung-ho.link.link
[3] https://enterjs.de/veranstaltung-78876-0-typescript--eine-anleitung-zum-ungluecklichsein.html?wt_mc=intern.academy.dpunkt.konf_dpunkt_vo_enterJS.empfehlung-ho.link.link
[4] https://enterjs.de/veranstaltung-82655-0-typescript-funktionen-dekorieren-nicht-nur-zu-weihnachten.html?wt_mc=intern.academy.dpunkt.konf_dpunkt_vo_enterJS.empfehlung-ho.link.link
[5] https://enterjs.de/veranstaltung-42559-0-hands-on-workshop-eigene-ki-agenten-mit-langchain.js-%26-langgraph.js.html?wt_mc=intern.academy.dpunkt.konf_dpunkt_vo_enterJS.empfehlung-ho.link.link
[6] https://enterjs.de/tickets.php?wt_mc=intern.academy.dpunkt.konf_dpunkt_vo_enterJS.empfehlung-ho.link.link
[7] https://github.com/microsoft/typescript-go
[8] https://www.youtube.com/watch?v=NrEW7F2WCNA
[9] mailto:mai@heise.de