Große TypeScript-Upgrades zerschießen Ihre App nicht in Produktion. Sie zerschießen Ihre CI, Ihren Editor – und Ihren Zeitplan. TypeScript 7 wird keine Ausnahme sein. Der Type-Checker wird strenger, alte Auswege schließen sich, und Ihr Monorepo leuchtet rot auf, weil Union-Narrowing an der einen Ecke smarter wurde und Ihr Decorators-Plugin an einer anderen eine Woche hinterherhinkt. Wenn Sie das wie ein lockeres Freitags-Dependency-Update behandeln, verlieren Sie einen Sprint.
Das ist kein Rant gegen Upgrades. Es ist ein Playbook, wie Sie auf TypeScript 7 migrieren, ohne die Auslieferung zu stoppen. Ziel: Features shippen, während Sie den Compiler umlegen. Das bedeutet zwei Pipelines (Build vs. Types), gestaffelte Flags, eine Baseline, die auf Null tendiert, und ein kleines, verantwortliches Tiger-Team. Wenn Sie die Technik in einem US-Startup mit 300K–2M LOC TypeScript verantworten, lesen Sie weiter.
Warum dieses Upgrade für CTOs wichtig ist
- Compiler-Semantik verschiebt sich bei Major-Releases. Selbst wenn das Runtime-Output identisch ist, kann eine Änderung in der Control-Flow-Analyse oder der JSX-Typisierung Tausende Zeilen ungültig machen. Ihr rotes CI ist kein Bug; es ist die neue Realität.
- Ihr Ökosystem bewegt sich mit Ihnen. Frameworks, Linter und Type-Packages koppeln Support oft an die Major-Version. Wenn Ihr Produkt von Next.js, Vite oder einem Designsystem abhängt, das Tooling pinnt, werden Sie am Ende zum Wechsel gezwungen.
- Die Kosten sind real, aber begrenzt. In der Praxis sehen wir 0,5–1,5 FTE‑Wochen pro 100K LOC für die Migration einer gesunden Codebasis mit reifer Pipeline. In einem Monorepo mit 1 Mio. LOC sind das 1–3 fokussierte Wochen für ein kleines Platform-Pod.
Was Teams typischerweise aus dem Tritt bringt (unabhängig von Versionsdetails)
- Drift bei der Modulauflösung. ESM/CJS-Dual-Packages, "exports"-Maps und unterschiedliche moduleResolution-Modi destabilisieren path aliases und Deep-Imports. Ihre Jest/Vitest-Konfiguration hinkt der Build-Konfiguration oft hinterher.
- Strengere Default-Flags oder engeres Inference. Ein smarter Checker deckt unsichere Muster auf, auf die Sie sich verlassen haben. 70–80 % der Fehler fallen meist in einige wenige Anti-Pattern.
- Decorators und Metadaten. Wenn Sie ORMs oder Frameworks mit Decorators nutzen, sind Sie Geisel deren Release-Kadenz. Ein kleiner Lag kann ein großes Compiler-Upgrade blockieren.
- Kopplung von Build und Types. Babel/esbuild/swc transpiliert problemlos, während tsc fehlschlägt – oder umgekehrt. Teams, die Emit an das Type-Checking gebunden haben, müssen eine Seite priorisieren, um die andere zu entkoppeln.
Die Meta-Strategie: zwei Pipelines, eine Baseline
Hören Sie auf, Kompilierung mit Type-Checking zu vermischen. Ihre Runtime-Pipeline darf nicht von der Tagesform des Checkers abhängen.
- Build und Types trennen. Nutzen Sie esbuild oder swc für Transpile/Bundle. Führen Sie tsc --noEmit für das Type-Checking aus. Halten Sie sie in separaten CI-Jobs mit unabhängiger Caching-Strategie und Ownership.
- Eine TypeScript-"duet"-Phase hinzufügen. Für ein paar Wochen laufen TypeScript 6 (aktuell) und TypeScript 7 (Upgrade) parallel in der CI. TS6 bleibt das Gate. TS7 schreibt in eine Baseline-Datei, die täglich sinken muss.
- Baseline etablieren. Beim ersten TS7-Lauf: alle Fehler in einer Baseline erfassen. Nicht pauschal global wegwinken. Zielgerichtete, reviewte Suppressions im Code erstellen – mit klarem Ablaufdatum.
Hygiene vor der Migration (ein Sprint langweilige Arbeit, der sich lohnt)
1) Bestandsaufnahme der Codebasis
- Größe und Zuschnitt: LOC pro Package zählen. Top‑20‑Packages nach Kompilierzeit und Fehlerrate identifizieren, sobald Sie einen Test-Build umlegen. Sie brauchen die Karte, bevor Sie die Schlachten wählen.
- Type‑Lücken: any-Nutzung messen. Wenn mehr als 10–15 % der Dateien auf implizites any oder aggressives as any setzen, setzen Sie Erwartungen: Die Migration wird viel Rot zutage fördern.
- Test-Runner-Footprint: Wie viele Jest‑Configs? Legacy ts-jest im Einsatz? Falls ja, planen Sie den Umstieg auf ts-jest mit isoliertem Transpile oder den Wechsel zu Vitest mit esbuild/swc, um Typkopplung zu vermeiden.
2) Modulauflösung entwirren
- Auf NodeNext-Semantik standardisieren. Gemischte ESM/CJS‑Packages sind Realität. tsconfig, Node-Runtime, Bundler und Test-Runner auf dieselbe Modulgeschichte ausrichten. Nicht lokal etwas anderes testen als Sie in Produktion shippen.
- Deep-Import-Gewohnheiten abstellen. Imports wie lib/something/internal über Paketgrenzen hinweg verbieten. Öffentliche Entry Points nutzen. Eine Linter-Regel hinzufügen, die Deep-Imports einfängt.
- Path-Aliases konsistent halten. Wenn tsconfig.paths @app/foo auf src/foo mappt, müssen Bundler und Test-Runner das identisch auflösen. Drift hier sorgt bei Upgrades für spooky action at a distance.
3) Project References nutzen
Wenn Sie ein Monorepo betreiben und keine TypeScript Project References verwenden, lassen Sie 20–40 % Compile-Zeit liegen – und Ihnen fehlen Stellhebel bei Upgrades. Composite-Builds liefern Paketgrenzen und Cacheability, die Red/Green-Zyklen überlebbar machen.
- Packages composite machen. composite: true setzen und Grenzen definieren. .d.ts für Leaf-Packages generieren, die Typen exportieren.
- tsc -b lokal und in der CI nutzen. Der Build-Mode erkennt Zyklen und steuert Invalidation. In einem Repo mit 1 Mio. LOC sinkt die Cold‑Build‑Zeit mit References und Caching typischerweise von 15–20 Minuten auf 6–10.
4) Strengere Flags vor dem Sprung stufenweise aktivieren
Überraschungen reduzieren Sie, indem Sie strengere Checks schon heute aktivieren. Wählen Sie 2–3 Flags, die Ihre Codebasis verkraftet, und rampen Sie sie mit Waivern:
- noUncheckedIndexedAccess, um unsichere Array-/Objektzugriffe sichtbar zu machen.
- exactOptionalPropertyTypes, um „fehlend“ nicht länger mit „undefined“ zu verwechseln.
- useUnknownInCatchVariables, um lazies any in Error-Handlern zu beenden.
Aktivieren Sie sie in einer Handvoll Packages mit einer klaren, teamweisen Quote an Fixes pro Woche. Der Effekt: geringerer Blast-Radius beim Umschalten auf TS7.
Die Migration, Schritt für Schritt
Phase 0 (1–2 Tage): Einfrieren und forken
- Alles einfrieren. Ihre Toolchain pinnen: Node-Version, Paketmanager, Bundler, Test-Runner und Type-Packages. Den Compiler upgraden und gleichzeitig Node oder Jest springen – so entstehen wochenlange Regressionen.
- Compiler forken. TypeScript 7 als separate Dev-Dependency in einem parallelen CI-Job hinzufügen. TypeScript 6 bleibt der gating Checker für main.
- Die TS7-Error-Baseline erstellen. Erster vollständiger Lauf von tsc --noEmit mit TS7 liefert Ihre „Schuld“. Speichern. Täglich veröffentlichen. Ihr Platform-Pod besitzt die Verantwortung, sie auf Null zu bringen.
Phase 1 (3–5 Tage): Die 80 % abtragen
- Fehler clustern. In einem Repo mit 500K–1M LOC erwarten Sie 1.000–5.000 initiale Fehler. 70–80 % sind wiederkehrende Muster. Was wir immer sehen: fehlende „satisfies“-Guards bei Objektliteralen, fragile Conditional Types, die durch strengeres Narrowing sichtbar werden, und Path-Alias-Leaks zur Runtime.
- Langweilige Fixes automatisieren. Mit ts-morph oder Codemods denselben lokalen Refactor über Dutzende Packages anwenden. Lint-Regeln hinzufügen, damit das Anti-Pattern nicht zurückwächst.
- Burn-down publizieren. Behandeln Sie es wie Incident-Abbau: tägliches Chart ins Unternehmen. Sie wollen bis Ende Woche 1 eine klare Linie Richtung Null.
Phase 2 (2–4 Tage): Die hartnäckigen 20 %
- Decorators und Ecosystem-Lag. Wenn Sie auf Decorator- oder Reflection-Plugins eines Frameworks setzen, koordinieren Sie mit diesem Team oder Vendor. Auf eine bekannte gute Version pinnen. Wenn Support aussteht, diese Packages hinter eine temporäre Grenze isolieren und nur dort waiven.
- Öffentliche API-Flächen. Bibliotheken, die .d.ts veröffentlichen, brauchen besondere Aufmerksamkeit. API Extractor laufen lassen, um die Stabilität der Public API nach dem Compiler-Wechsel zu verifizieren.
- Test-Runner-Parität. Sicherstellen, dass Jest/Vitest ESM/CJS genauso auflösen wie Bundler und Node. Mismatches erzeugen „läuft im Dev, fällt in CI“‑Fehler, die wie TypeScript-Regressions aussehen, in Wahrheit aber Modul-Bugs sind.
Phase 3 (1 Tag): Gate umschalten
- TS7 zum Gate machen. Wenn die Baseline bei Null ist und der Main-Branch 48 Stunden mit dem Duett-Job grün war, TS7 als gating Checker setzen. Den TS6‑Job noch eine Woche als Canary behalten, dann entfernen.
- Auftauen. Das Ökosystem vorsichtig entpinnen. Pro Tag eine Tool-Klasse upgraden (Linter, dann Test-Runner, dann Bundler), um kombinatorische Explosionen zu vermeiden.
Leitplanken für Performance und Zuverlässigkeit
- Alles cachen. Esbuild/swc-Output und tsc -b-Artefakte remote cachen. In einem Repo mit 1 Mio. LOC sinkt die CI-Zeit mit stabilen Cache-Keys um 30–50 %.
- skipLibCheck in der CI eingeschaltet lassen, außer Sie veröffentlichen Libraries. Dann einen separaten, nächtlichen „libcheck“-Job hinzufügen, um Drift in Type-Packages zu erkennen, ohne jeden PR zu belasten.
- Das Delta messen. Build-Zeit und Bundle-Größe vor/nachher tracken. In einer gesunden Codebasis erwarten wir tsc --noEmit-Wall-Time innerhalb von ±10 % und keine nennbaren Runtime-Änderungen. Größere Regressionen sind meist eine neue Type‑Level‑Rekursions-Explosion in einem einzelnen Package – identifizieren und vereinfachen.
People und Kostenmodell
Sie brauchen keinen Schwarm. Sie brauchen ein kleines Pod, das unglamouröse Änderungen schnell umsetzt und ruhig koordiniert.
- Team: 2–4 Senior Engineers aus Platform/Tooling mit der Autorität, alle Packages anzufassen. Dazu eine Vertretung Ihres größten Produktteams für Kontext.
- Zeit: 1–3 Wochen für 1 Mio. LOC. Ein gesundes Monorepo mit Project References und moderner Test-/Build-Stack liegt näher an einer Woche. Fehlen References und transpiliert tsc noch, planen Sie drei.
- Overlap zählt. Wenn Sie ein Nearshore-Pod in Brazil nutzen, erhalten Sie 6–8 Stunden US-Overlap für das tägliche Burn-down und Dependency‑Wrangling – und 20–30 % geringere Kosten für die Monotonie von Codemods und Config-Diffs.
Was brechen wird (und wie man es entschärft)
- Jest mit ts-jest in ESM-Repos. Wenn Sie Types noch durch Jest kompilieren, wechseln Sie zu nativem ESM mit Babel/esbuild für den Transform und überlassen Sie Types tsc --noEmit. Oder setzen Sie, wo möglich, auf Vitest.
- Path-Aliases zur Laufzeit. Imports wie @app/foo funktionieren im Editor, explodieren aber in Node. Lösen Sie sie im Bundler/Test auf und erzeugen Sie reale relative Imports im emittierten Code. Ein einfacher Preflight-Check „keine unerledigten Aliases“ spart Stunden.
- Barrel-Files und zirkuläre Deps. Der Checker wird besser darin, subtile Zyklen zu erkennen. dependency-cruiser laufen lassen und Zyklen vor dem Flip explizit aufbrechen.
- Ambiente Typ-Leaks aus Tests. Globale Deklarationen bluten in Monorepos mit schlampigen Globs in den Source-Code. Ihr types-Feld in tsconfig scopen und Test-Typen von Prod-Typen trennen.
Ein reales Ergebnismuster (anonymisiert)
Bei einem Fintech-Scale-up mit ~700K LOC TypeScript über 86 Packages haben wir ein zweiwöchiges TS-Major-Upgrade ohne Feature-Freeze gefahren:
- Woche 0: Project References zu 18 nachhängenden Packages hinzugefügt; Tests von ts-node weggezogen; Modulauflösung standardisiert.
- Woche 1: Dualer Checker in der CI (alt als Gate, neu als Baseline). 2.840 initiale TS7-Fehler; 2.250 mit drei Codemods und zwei Lints behoben. CI-Zeit nach References und Remote Caching von 28 auf 17 Minuten gesunken.
- Woche 2: Decorator-lastiges ORM‑Package 3 Tage blockiert, bis Vendor‑Patch kam; isoliert und lokal ge-waived. Baseline auf Null; TS7 zum Gate befördert. Keine Prod‑Incidents. Entwickler‑Feedback: weniger Phantom‑IntelliSense‑Fehler.
Wann Sie noch nicht migrieren sollten
- Ihr Kernframework ist nicht bereit. Wenn Ihre App von einem Framework abhängt, das noch keinen Support erklärt hat, warten Sie. Forken Sie kein eigenes Decorators‑Plugin, außer Sie wollen es dauerhaft tragen.
- Sie ändern gleichzeitig Node, Bundler oder Test-Runner. Trennen Sie die Änderungen. Eine Dimension nach der anderen. Andernfalls ist die Zuverlässigkeit gegen Sie.
- Ihre Typ-Schulden sind katastrophal. Wenn implizites any überall ist und Ihre Tests von ts-node abhängen, machen Sie erst einen Sprint Hardening. Das spart zwei in der eigentlichen Migration.
Governance: Erreichtes sichern
- Ein vierteljährliches Compiler-Bump-Ritual einführen. Auf die Platform-Roadmap setzen. Kleine Schritte schlagen mehrjährige Sprünge.
- Den Duett-Job behalten. Sie brauchen nicht dauerhaft zwei Majors, aber fahren Sie das Duett-Muster für jedes Minor/Major erneut. Es ist eine günstige Versicherung.
- Type Health als KPI tracken. Any-Rate, Anzahl Suppressions und Fehlerrate pro Package sind Frühindikatoren für künftige Upgrade-Schmerzen.
Wenn Sie Unterstützung möchten
Unsere Nearshore-Pods aus Brazil machen diese Arbeit das ganze Jahr über: Modulauflösung entwirren, Project References monorepo-weit ausrollen, fragile Muster per Codemod entfernen und die CI grün halten, während Sie shippen. Sie bekommen US-Overlap (6–8 Stunden), erprobte Checklisten und ein Team, das diese Dinge schon gebrochen (und gefixt) hat.
Wichtigste Erkenntnisse
- Build von Types trennen; esbuild/swc für Emit und tsc --noEmit fürs Checking fahren.
- Für 1–2 Wochen einen Dual-Checker in der CI laufen lassen (aktuell als Gate, TS7 als Baseline).
- Project References und Remote Caching einführen, um Compile-Zeiten zu bändigen.
- Eine einheitliche Modulgeschichte (NodeNext) über Runtime, Bundler und Tests standardisieren.
- Die repetitiven 70–80 % der Fehler mit Codemods fixen; die hartnäckigen 20 % isolieren.
- 0,5–1,5 FTE‑Wochen pro 100K LOC budgetieren; ein kleines Platform-Pod statt einer Herde einsetzen.
- Keine gleichzeitigen Upgrades von Node/Bundler/Test – eine Achse nach der anderen.
- Type Health als Produktmetrik behandeln, damit das nächste Upgrade langweilig wird.