Schrittweise statt Big Bang
Eine vollständige Neuschreibung einer gewachsenen JavaScript-Codebasis scheitert in der Praxis fast immer an Zeitdruck, laufendem Betrieb und Merge-Konflikten. Dieser Artikel zeigt, wie allowJs und checkJs Typfehler aufdecken, ohne eine einzige Datei umzubenennen, welche Reihenfolge sich bewährt und wie Teams die Migration messbar zu Ende bringen, statt in einem dauerhaften Zwischenzustand steckenzubleiben.
Inhaltsverzeichnis
- 1. Warum Big-Bang-Rewrites an der Realität scheitern
- 2. allowJs und checkJs: Typfehler finden, ohne umzubenennen
- 3. JSDoc-Typannotationen als Zwischenschritt in .js-Dateien
- 4. Leaf-First: die praktische Migrationsreihenfolge
- 5. Dateien umbenennen und Strict-Flags gezielt aktivieren
- 6. Fortschritt messen: type-coverage und CI-Gates
- 7. Häufige Fallstricke: any, @ts-ignore und fehlende Typen
- 8. Team-Prozess: Code-Review-Regeln für Mehrpersonen-Migrationen
- 9. Big Bang gegen schrittweise Migration im Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Warum Big-Bang-Rewrites an der Realität scheitern
Die Idee, eine JavaScript-Codebasis in einem einzigen großen Schritt komplett nach TypeScript zu übersetzen, klingt auf dem Whiteboard sauber, scheitert aber in gewachsenen Projekten fast immer an drei Faktoren: Zeitdruck, Merge-Konflikten und dem laufenden Betrieb. Ein Rewrite bindet ein Team über Wochen oder Monate, während parallel neue Features verlangt werden - der Feature-Branch driftet unweigerlich vom Hauptzweig weg und die Zusammenführung wird zum eigenen Projekt. In der Zwischenzeit entstehen in beiden Branches Bugfixes, die doppelt gepflegt werden müssen.
Hinzu kommt ein psychologischer Effekt: Ein Big-Bang-Rewrite hat keinen sichtbaren Zwischenerfolg. Entweder er ist fertig, oder er liefert wochenlang keinen Mehrwert - ein klassisches Muster, das Management-Unterstützung erodieren lässt, sobald andere Prioritäten dazwischenkommen. Schrittweise Migration kehrt dieses Risiko um: Jede migrierte Datei liefert sofort messbaren Nutzen in Form von Typsicherheit, ohne dass der Rest der Anwendung angefasst werden muss. Der Code bleibt zu jedem Zeitpunkt lauffähig, testbar und deploybar, weil TypeScript reines JavaScript als gültige Eingabe akzeptiert und die Migration schrittweise, Datei für Datei, ohne Funktionsstillstand erfolgen kann.
2. allowJs und checkJs: Typfehler finden, ohne umzubenennen
Der erste konkrete Schritt jeder inkrementellen Migration ist nicht das Umbenennen einer einzigen Datei, sondern das Hinzufügen einer tsconfig.json mit den Flags allowJs und checkJs. allowJs erlaubt dem TypeScript-Compiler, .js-Dateien als Teil des Projekts zu behandeln und mit .ts-Dateien zu verlinken. checkJs geht einen Schritt weiter und aktiviert die Typprüfung auch innerhalb dieser unveränderten .js-Dateien - basierend auf Typinferenz und vorhandenen JSDoc-Kommentaren, ganz ohne dass eine Zeile Code umgeschrieben werden muss.
Damit lässt sich der Ist-Zustand einer Codebasis objektiv erfassen, bevor überhaupt migriert wird: tsc --noEmit läuft über das gesamte Projekt und listet alle Typfehler auf, die bereits mit der bestehenden Codebasis auftreten - oft sind das erschreckend viele, weil implizite any-Typen, inkonsistente Rückgabewerte und stillschweigend falsch aufgerufene Funktionen plötzlich sichtbar werden. Wichtig ist, checkJs zunächst projektweit non-blocking laufen zu lassen, also ohne den Build zu brechen, und stattdessen die Fehlerzahl als Ausgangsmetrik zu dokumentieren, an der sich der Fortschritt der Migration später messen lässt.
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Bundler",
"allowJs": true,
"checkJs": true,
"strict": false,
"noEmit": true,
"skipLibCheck": true,
"resolveJsonModule": true,
"esModuleInterop": true
},
"include": ["src/**/*.js", "src/**/*.ts"],
"exclude": ["node_modules", "dist", "**/*.test.js"]
}
3. JSDoc-Typannotationen als Zwischenschritt in .js-Dateien
Sobald checkJs aktiv ist, lassen sich einzelne .js-Dateien mit JSDoc-Kommentaren typisieren, ohne die Dateiendung zu ändern. Das ist der entscheidende Zwischenschritt, der Big-Bang-Rewrites überflüssig macht: Funktionssignaturen, Parameter und Rückgabewerte bekommen präzise Typen, während die Datei weiterhin als reines JavaScript läuft, von jedem Build-Tool ohne zusätzlichen Transpile-Schritt verarbeitet wird und für Kollegen ohne TypeScript-Erfahrung unverändert lesbar bleibt. Der TypeScript-Compiler liest JSDoc-Typen genauso ernst wie echte Typannotationen und meldet Verstöße mit derselben Präzision.
Besonders wertvoll ist dieser Ansatz für Utility-Funktionen und Helfer, die von vielen Stellen aus aufgerufen werden: Eine falsch typisierte Parameterreihenfolge oder ein vergessener optionaler Parameter fällt sofort im Editor auf, lange bevor die Datei überhaupt zu .ts umbenannt wird. Für komplexere Strukturen wie Objektformen oder Union-Typen lassen sich @typedef-Blöcke definieren und projektweit über import()-Typen in JSDoc referenzieren - eine vollwertige Typablage, ganz ohne eine einzige .ts-Datei anzulegen.
// @ts-check
// utils/formatPrice.js - fully typed via JSDoc, still plain JavaScript
/**
* @typedef {Object} PriceOptions
* @property {string} currency
* @property {number} [decimals]
*/
/**
* Formats a numeric price value into a localized currency string.
* @param {number} amount
* @param {PriceOptions} options
* @returns {string}
*/
function formatPrice(amount, options) {
const decimals = options.decimals ?? 2;
return new Intl.NumberFormat('de-DE', {
style: 'currency',
currency: options.currency,
minimumFractionDigits: decimals,
}).format(amount);
}
module.exports = { formatPrice };
4. Leaf-First: die praktische Migrationsreihenfolge
Die Reihenfolge, in der Dateien migriert werden, entscheidet über Erfolg oder Frust der gesamten Migration. Bewährt hat sich ein Leaf-First-Ansatz: zuerst reine Utility-Module ohne oder mit minimalen internen Abhängigkeiten, dann Service- und Datenzugriffsschichten, danach UI-Komponenten, und ganz zuletzt Entry-Points und Bootstrapping-Code. Der Grund liegt in der Abhängigkeitsrichtung: Eine Utility-Funktion wird von vielen anderen Modulen importiert, hat selbst aber kaum Importe. Wird sie zuerst typisiert, profitieren automatisch alle Aufrufer von präziseren Typen, ohne dass diese Aufrufer selbst schon migriert sein müssen.
Würde man stattdessen mit dem Entry-Point beginnen, säße man sofort auf einem Berg ungelöster Abhängigkeiten zu noch untypisierten Modulen und müsste an jeder Stelle mit any oder pauschalen Type-Assertions arbeiten - genau das Muster, das später als technische Schuld liegen bleibt. Ein Dependency-Graph-Tool wie madge oder dependency-cruiser hilft, die tatsächliche Importstruktur eines Projekts sichtbar zu machen und Module ohne zyklische Abhängigkeiten als sichere erste Kandidaten zu identifizieren. Zirkuläre Abhängigkeiten sollten grundsätzlich vor der Migration aufgelöst werden, da sie sowohl bei der Typisierung als auch beim Modulsystem selbst Probleme verursachen.
# Dependency graph analysieren, um Leaf-Module zu identifizieren
npx madge --circular src/
npx madge --image graph.svg src/index.js
# Module ohne eingehende projektinterne Importe sind die ersten Kandidaten
npx dependency-cruiser --output-type err-long src \
--config .dependency-cruiser.js
5. Dateien umbenennen und Strict-Flags gezielt aktivieren
Erst wenn eine Datei über JSDoc bereits weitgehend typisiert und typfehlerfrei ist, folgt der eigentliche Umbenennungsschritt von .js zu .ts. Das ist meist ein kleiner, risikoarmer Commit, weil die Typinformationen bereits vorhanden sind und lediglich in native TypeScript-Syntax überführt werden - JSDoc-Kommentare werden zu echten Typannotationen, module.exports wird zu ES-Modul-Syntax, sofern das Projekt nicht ohnehin schon ESM nutzt. Jede Umbenennung sollte als eigener, klein gehaltener Commit erfolgen, der ausschließlich diese eine Datei betrifft, damit Code-Reviews überschaubar bleiben und Git-History bei Bedarf sauber nachvollziehbar ist.
Strict-Flags wie strictNullChecks oder noImplicitAny müssen dabei nicht sofort projektweit aktiv sein. TypeScript erlaubt es, Strictness pro Verzeichnis über verschachtelte tsconfig.json-Dateien mit extends zu staffeln: Bereits migrierte Verzeichnisse laufen unter voller Strenge, während der Rest der Codebasis vorerst mit lockereren Einstellungen weiterläuft. So wächst die Typsicherheit graduell mit dem Migrationsfortschritt, statt an einem einzigen Stichtag das gesamte Projekt gegen hunderte neue Fehler zu werfen.
// utils/formatPrice.ts - renamed from .js, JSDoc becomes native types
export interface PriceOptions {
currency: string;
decimals?: number;
}
export function formatPrice(amount: number, options: PriceOptions): string {
const decimals = options.decimals ?? 2;
return new Intl.NumberFormat('de-DE', {
style: 'currency',
currency: options.currency,
minimumFractionDigits: decimals,
}).format(amount);
}
// tsconfig.json in src/utils overrides the project-wide config
// {
// "extends": "../../tsconfig.json",
// "compilerOptions": {
// "strict": true,
// "noImplicitAny": true
// }
// }
6. Fortschritt messen: type-coverage und CI-Gates
Eine Migration ohne messbaren Fortschritt verliert schnell an Priorität, sobald andere Aufgaben drängen. Das Tool type-coverage berechnet den prozentualen Anteil an Code-Positionen mit einem konkreten, nicht-any-Typ und liefert damit eine einzelne, leicht kommunizierbare Kennzahl, die sich pro Sprint tracken lässt. Ergänzend zählt ein einfaches Skript, wie viele .js- gegenüber .ts-Dateien im Projekt verbleiben - eine grobe, aber für Stakeholder gut verständliche Fortschrittsanzeige.
Der wirkungsvollste Hebel gegen Stillstand ist jedoch ein CI-Gate, das neue .js-Dateien in bereits migrierten Verzeichnissen aktiv verhindert. Ohne eine solche Sperre schleicht sich unter Zeitdruck immer wieder neuer, untypisierter Code in eigentlich migrierte Bereiche ein, und der Fortschrittsbalken bewegt sich rückwärts. Ein einfacher Lint-Check oder ein kleines Node-Skript, das in der Pipeline nach neu hinzugefügten .js-Dateien außerhalb definierter Ausnahmen sucht, reicht dafür meist aus und lässt sich in wenigen Zeilen implementieren.
#!/usr/bin/env bash
# ci/check-type-coverage.sh - fail the pipeline below a coverage threshold
set -euo pipefail
THRESHOLD=85
COVERAGE=$(npx type-coverage --detail --strict | tail -1 | grep -oP '\d+(?=\.\d+%)')
echo "Current type coverage: ${COVERAGE}%"
if [ "$COVERAGE" -lt "$THRESHOLD" ]; then
echo "Type coverage below threshold of ${THRESHOLD}%. Failing build."
exit 1
fi
# Reject new plain .js files inside already-migrated directories
if git diff --name-only --diff-filter=A origin/main...HEAD \
| grep -E '^src/(utils|services)/.*\.js$'; then
echo "New .js files are not allowed in migrated directories."
exit 1
fi
7. Häufige Fallstricke: any, @ts-ignore und fehlende Typen
Der häufigste Rückschritt in einer laufenden Migration ist implizites any, das über ungetypte Funktionsparameter oder Rückgabewerte aus noch nicht migriertem Code zurück in bereits typisierte Bereiche fließt. Ohne noImplicitAny akzeptiert TypeScript solche Lücken stillschweigend, und die Typsicherheit einer eigentlich sauber migrierten Datei wird von außen ausgehöhlt. Regelmäßige type-coverage-Läufe mit Trend-Reporting decken solche schleichenden Regressionen zuverlässig auf, lange bevor sie zum echten Problem werden.
Eine zweite Falle ist die unkontrollierte Anhäufung von @ts-ignore-Kommentaren als schnelle Lösung für lästige Fehler unter Zeitdruck. Jedes @ts-ignore ist ein bewusst blinder Fleck im Typsystem und sollte niemals kommentarlos gesetzt werden - ein begleitender Grund im Code sowie ein Ticket-Verweis machen daraus zumindest sichtbare, nachverfolgbare Schuld statt stiller Dunkelheit. @ts-expect-error ist in den meisten Fällen die bessere Wahl, weil es einen Compiler-Fehler wirft, sobald der unterdrückte Fehler nicht mehr existiert - ein eingebauter Reminder, die Unterdrückung wieder zu entfernen. Drittanbieter-Bibliotheken ohne eigene Typdefinitionen lassen sich über das Paket @types/* aus DefinitelyTyped nachrüsten, notfalls mit einer eigenen minimalen .d.ts-Deklaration statt einem pauschalen any für das gesamte Modul.
8. Team-Prozess: Code-Review-Regeln für Mehrpersonen-Migrationen
Sobald mehrere Entwickler parallel an derselben Migration arbeiten, wird Koordination zur eigentlichen Herausforderung. Ohne klare Absprache migrieren zwei Personen möglicherweise dieselbe Datei gleichzeitig, oder eine Umbenennung von .js zu .ts kollidiert mit einem inhaltlichen Feature-Branch auf derselben Datei und erzeugt Merge-Konflikte, die Git bei reinen Dateiumbenennungen eigentlich gut auflösen könnte, bei gleichzeitigen inhaltlichen Änderungen aber nicht mehr zuverlässig trennt. Ein einfaches Board oder eine geteilte Liste, welche Verzeichnisse aktuell „in Migration“ sind, verhindert die meisten dieser Kollisionen.
Für Code-Reviews hat sich eine feste Regel bewährt: Reine Umbenennungs-Commits ohne inhaltliche Änderung werden separat von Feature-Commits reviewt und idealerweise mit git mv statt Löschen und Neuanlegen durchgeführt, damit die Diff-Ansicht die Umbenennung erkennt statt eine komplette Datei als neu anzuzeigen. Neue Pull Requests, die Code in bereits migrierten Verzeichnissen ändern, sollten grundsätzlich in TypeScript statt in JavaScript eingereicht werden müssen - eine einfache, im Review-Template dokumentierte Regel, die verhindert, dass der Migrationsfortschritt durch alltägliche Feature-Arbeit wieder verwässert wird.
9. Big Bang gegen schrittweise Migration im Vergleich
Die folgende Übersicht stellt riskante Big-Bang-Muster den empfohlenen inkrementellen Alternativen gegenüber, die sich in laufenden Magento- und Node.js-Projekten in der Praxis bewährt haben.
| Aspekt | Big-Bang-Ansatz (riskant) | Schrittweiser Ansatz (empfohlen) |
|---|---|---|
| Umfang pro Schritt | Gesamte Codebasis auf einmal umschreiben | Eine Datei oder ein Modul pro Commit |
| Strict-Flags | strict: true sofort projektweit erzwingen | Pro Verzeichnis via extends staffeln |
| Betriebsrisiko | Langer Feature-Branch, hohes Merge-Risiko | main bleibt jederzeit deploybar |
| Fortschrittssichtbarkeit | Kein Mehrwert bis zum Abschluss | type-coverage steigt sichtbar pro Sprint |
| Fehlerbehandlung | Hunderte Fehler an einem Stichtag | checkJs deckt Fehler früh und lokal auf |
Der entscheidende Unterschied liegt nicht im Endzustand - beide Wege sollen zu einer vollständig typisierten Codebasis führen -, sondern im Risiko unterwegs. Die schrittweise Migration hält das Projekt zu jedem Zeitpunkt lauffähig und lässt sich bei Bedarf jederzeit pausieren, ohne dass ein halb fertiger Rewrite als Ballast zurückbleibt.
Mironsoft
TypeScript-Migrationen und typsichere Frontend-Architektur für Magento und Hyvä
JavaScript-Codebasis schrittweise zu TypeScript migrieren?
Wir analysieren eure Codebasis, definieren eine realistische Leaf-First-Reihenfolge und richten allowJs, checkJs und CI-Gates so ein, dass die Migration messbar vorankommt, ohne den laufenden Betrieb zu gefährden.
Migrations-Audit
Dependency-Graph, Ausgangs-Type-Coverage und Risikobewertung pro Modul
tsconfig-Setup
allowJs, checkJs und gestaffelte Strict-Flags pro Verzeichnis
CI/CD-Gates
type-coverage-Schwellenwerte und Schutz bereits migrierter Verzeichnisse
10. Zusammenfassung
Eine JavaScript-zu-TypeScript-Migration gelingt nicht durch einen mutigen Rewrite, sondern durch konsequente kleine Schritte, die die Codebasis zu jedem Zeitpunkt lauffähig lassen. allowJs und checkJs machen Typfehler in bestehenden .js-Dateien sichtbar, ohne dass eine einzige Datei umbenannt werden muss. JSDoc-Typannotationen liefern echte Typsicherheit als Zwischenschritt, bevor die eigentliche Umbenennung zu .ts erfolgt. Eine Leaf-First-Reihenfolge, beginnend bei Utility-Modulen und endend bei Entry-Points, sorgt dafür, dass jede migrierte Datei sofort Mehrwert für ihre Aufrufer schafft.
Der entscheidende Erfolgsfaktor gegen den „halb migriert für immer“-Zustand ist Messbarkeit: type-coverage als Kennzahl, CI-Gates gegen neue .js-Dateien in migrierten Bereichen und klare Team-Regeln für Umbenennungs-Commits verhindern, dass die Migration im Alltagsgeschäft versandet. Wer diese Bausteine konsequent kombiniert, erreicht eine vollständig typisierte Codebasis, ohne den Betrieb je zu gefährden.
JavaScript-zu-TypeScript-Migration - Das Wichtigste auf einen Blick
allowJs + checkJs zuerst
Typfehler in bestehenden .js-Dateien aufdecken, ohne eine Zeile Code umzuschreiben oder umzubenennen.
Leaf-First migrieren
Utility-Module vor Services, Services vor Komponenten, Komponenten vor Entry-Points.
Fortschritt messen
type-coverage als Kennzahl und CI-Gates gegen neue .js-Dateien in migrierten Bereichen.
Fallen vermeiden
Implizites any, unkommentierte @ts-ignore und fehlende Typen für Drittanbieter-Pakete konsequent verhindern.