Startseite / Artikel / Sprache nach Problemtyp auswählen: Lektionen aus der Umsetzung von TypeScript in Go

Sprache nach Problemtyp auswählen: Lektionen aus der Umsetzung von TypeScript in Go

Was die Wahl von Go als nativer TypeScript-Kompilierer über das Passen von Werkzeugen zu den jeweiligen Anforderungen, die Bedeutung der Geschwindigkeit von Werkzeugen sowie das schrittweise Portieren großer Codebasen lehrt.

1223 Wörter

Fast eine Minute auf die Autocomplete-Funktion in einem großen TypeScript-Monorepo warten ist ein bekanntes Problem, und Mobile-Entwickler erleben denselben Ärger, wenn Xcode ein großes Projekt indiziert. Das TypeScript-Team hat genau dieses Problem angegangen, und die Art und Weise, wie es es gelöst hat, ist eine nützliche Fallstudie dafür, technologische Entscheidungen auf Grundlage von Fakten statt aus Modetrends zu treffen. Dieser Artikel blickt über die Benchmark-Meldungen hinaus und zeigt die ingenieurtechnischen Überlegungen, die Sie auf Ihre eigenen Tools und Apps anwenden können – egal ob Sie TypeScript, Swift oder beides einsetzen.

Was Microsoft angekündigt hat

Im März 2025 kündigte Anders Hejlsberg, der sowohl TypeScript als auch C# entwickelt hat, an, dass Microsoft eine native Portierung des TypeScript-Kompilators sowie seiner Sprachwerkzeuge erstellt. Viele nahmen an, das Ziel wäre Rust oder vielleicht C++. Es war Go.

was die Go-Umgestaltung ohne Änderungen am Code bewirkt separat behandelt. Der Rest dieses Artikels konzentriert sich auf die Entscheidung selbst.

Warum Go statt Rust

Das Team suchte nach der Sprache niedrigster Ebene, die trotzdem native Code auf allen Plattformen erzeugen konnte, die TypeScript unterstützen muss, und gleichzeitig eine gute eingebaute Konkurrenzfähigkeit bot. Go entsprach dieser Beschreibung.

Zwei weitere Faktoren waren wichtig. Erstens die Arbeitslast: Die Typüberprüfung eines großen Projekts besteht größtenteils darin, viele Dateien parallel zu überprüfen und anschließend die Ergebnisse zusammenzufassen – Goroutinen entsprechen diesem Muster sehr direkt. Zweitens der vorhandene Code: Der Compiler basiert auf einer riesigen JavaScript-Codebasis, die von einer Garbage Collection sowie einem eher funktionalen Stil abhängt. Go verfügt ebenfalls über eine Garbage Collection und eine ähnliche Struktur, sodass der Code nahezu unverändert übersetzt werden konnte. Ein Umstieg auf Rust hätte einen viel umfangreicheren Neuentwurf hinsichtlich Eigentumsverhältnissen und Lebenszeiten erfordert, was für ein großes Team mit einer enormen Codebasis einen hohen Aufwand bedeutet hätte.

Einfach ausgedrückt wählte das Team nicht die modischste Sprache. Es wählte stattdessen diejenige, die zu seinem Konkurrenzmodell passte und seine Entwickler produktiv hielt. Das ist ein architektonisches Urteil – derselbe Art von Entscheidung, die man trifft, wenn man zwischen UIKit und SwiftUI oder zwischen Combine und herkömmlichem async/await für eine bestimmte Funktion entscheidet.

Dieselbe Idee in Swift

Swift-Entwickler sind bereits früher auf dieses Problem gestoßen. Strukturierte Konkurrenz wurde in Swift genau deshalb hinzugefügt, um aufwändige Aufgaben parallel auszuführen, ohne die Korrektheit zu gefährden. Der folgende Codeausschnitt veranschaulicht das Konzept „mehrere Dateien gleichzeitig überprüfen und anschließend zusammenführen“ mithilfe einer Task-Gruppe: Jede Datei erhält ihre eigene Unteraufgabe, und die übergeordnete Aufgabe sammelt die Ergebnisse ein, sobald die Unteraufgaben abgeschlossen sind. Beachten Sie, dass es sich um Swift handelt, obwohl damit das Muster veranschaulicht wird, das der Go-Compiler mit Goroutinen verwendet.

// Swift concurrency: parallel "type-checking" of files, similar in spirit
// to how Go's goroutines let TypeScript check many files at once
func checkFiles(_ paths: [String]) async -> [Diagnostic] {
    await withTaskGroup(of: [Diagnostic].self) { group in
        for path in paths {
            group.addTask {
                await checkSingleFile(path) // runs concurrently
            }
        }
        var results: [Diagnostic] = []
        for await diagnostics in group {
            results.append(contentsOf: diagnostics)
        }
        return results
    }
}

Mehrere Punkte sind erwähnenswert. withTaskGroup stellt sicher, dass jede Unteraufgabe vor dem Rückgang der Funktion abgeschlossen wird, sodass keine Arbeit außerhalb des Scope bleibt. Die Ergebnisse kommen in der Reihenfolge ihrer Abschluss, nicht in der Eingabereihenfolge – daher müssen Sie sie ggf. nachträglich sortieren, wenn die Reihenfolge der Diagnosen wichtig ist. Zudem ist die Parallelisierung nur sicher, weil jede Überprüfung unabhängig ist; genau diese Eigenschaft macht die Typüberprüfung zu einem guten Fit für Go’s Modell.

Falls Sie Swift 6 verwendet haben, haben Sie bereits diesen Kompromiss eingegangen: Sie zahlen im Voraus für strenge Konkurrenz- und Sicherheitsprüfungen, erhalten aber im Gegenzug schnellere Kompiliervorgänge sowie stabileres Laufzeitverhalten. Microsoft hat mit seinem Compiler einen ähnlichen Ansatz gewählt.

Lektionen, die auch auf Ihre eigenen Projekte übertragen werden können

  • Die Geschwindigkeit der Entwicklungstools ist eine Produktmerkmal. Langsame Kompiliervorgänge und träge Autocomplete-Funktionen verursachen jeden Tag eine weitgehend unsichtbare Belastung für alle Entwickler in einem Team. Arbeiten an der Leistung von Build-Pipelines, Editoren und Indexierungsmechanismen sind genauso wichtig wie Arbeiten an der Leistung der fertiggestellten Anwendung.
  • Lassen Sie das Problem – nicht die Trends – das Tool bestimmen. Egal, ob Sie sich für MVVM, SwiftData oder Combine entscheiden – die richtige Wahl ergibt sich aus der tatsächlichen Datenflussstruktur in Ihrem System und nicht daraus, was gerade in den sozialen Medien beliebt ist.
  • Überarbeitungen können schrittweise erfolgen. Das TypeScript-Team portierte den bestehenden Compiler so getreu wie möglich, anstatt ihn neu zu entwerfen, sodass die neue Version dieselben Ergebnisse liefert wie die alte. Das ist ein starker Argument dagegen, eine ganze Anwendungsarchitektur in einem einzigen Sprint neu aufzubauen.

Wann sollten Sie diesem Beispiel nicht folgen? Wenn Ihr aktueller Stack nicht das Engpassproblem ist, bringt ein Umstieg nur Risiken mit sich. Messen Sie zunächst und stellen Sie sicher, dass der langsame Teil tatsächlich in der Schicht liegt, die Sie ersetzen möchten.

Erkennen der Warnsignale

Die Symptome, die zu diesem Projekt führten, treten in jeder großen Codebasis auf: verlangsamte Typinferenz, verzögerte Editor-Feedbacks sowie stetig wachsende Build-Graphen. Die Lösung ist nicht immer ein neues Framework. Manchmal bedeutet es, genauer hinzuschauen, was Ihre Tools im Hintergrund tatsächlich tun, und den Teil zu ändern, der zu viel Arbeit leistet.

Kernpunkte

  • Go wurde gewählt, weil seine native Kompilierung, der Garbage Collector sowie die auf Goroutinen basierende Konkurrenzfähigkeit sowohl zur Typüberprüfung als auch zur Struktur des bestehenden Codes passten.
  • Die Beschleunigungen resultierten von einer getreuen Portierung, die das Verhalten konstant hielt, während sich die zugrunde liegende Ausführung veränderte.
  • Betrachten Sie die Latenz der Entwicklerwerkzeuge als echten Kostenfaktor, für den sich ingenieurtechnische Anstrengungen lohnen.
  • Nächstes Mal, wenn ein Build langsam ist, stellen Sie sich die Frage, die das TypeScript-Team gestellt hat: Welches Werkzeug passt tatsächlich zu der Art dieses Problems?
  • Verwandte Artikel