Eine schrittweise Strategie für gewachsene Codebasen statt Big-Bang-Umbau
Ein Projekt, das jahrelang ohne strictNullChecks gewachsen ist, hat mit hoher Wahrscheinlichkeit hunderte oder tausende Stellen, an denen null oder undefined nicht behandelt werden. Die Option strictNullChecks nachträglich einzuschalten, bricht in so einem Projekt meist sofort den Build mit einer erdrückenden Fehlerliste, weshalb eine schrittweise, ordnerweise Migration fast immer der einzig realistische Weg ist.
Inhaltsverzeichnis
- 1. Warum ein direkter Umschalter meist scheitert
- 2. Opt-in statt Opt-out: strict per Datei aktivieren
- 3. ts-migrate und ähnliche Werkzeuge für die Erstbehandlung
- 4. Die vier häufigsten Fehlerklassen und wie man sie behebt
- 5. Der Non-Null-Assertion-Operator als kontrolliertes Ventil
- 6. Module priorisieren: wo zuerst migrieren
- 7. Ein CI-Gate gegen Rückschritte
- 8. Team-Koordination während einer länger laufenden Migration
- 9. Realistische Zeitplanung und Erfolgskriterien
- 10. Zusammenfassung
- 11. FAQ
1. Warum ein direkter Umschalter meist scheitert
strictNullChecks ändert die Bedeutung von null und undefined fundamental: Ohne die Option sind beide Werte implizit Teil jedes Typs, mit der Option müssen sie explizit in der Signatur auftauchen. In einer Codebasis mit mehreren zehntausend Zeilen führt das Einschalten oft zu mehreren tausend neuen Fehlern auf einen Schlag, weil jede Stelle betroffen ist, an der ein Wert theoretisch null sein könnte.
Ein Big-Bang-Versuch, alle Fehler in einem einzigen Pull-Request zu beheben, blockiert das Team über Tage oder Wochen und führt fast immer zu einem riesigen, kaum review-baren Diff. Realistischer ist eine Strategie, bei der strictNullChecks für neue Dateien sofort gilt, während bestehender Code schrittweise, Datei für Datei oder Ordner für Ordner, migriert wird.
2. Opt-in statt Opt-out: strict per Datei aktivieren
Der pragmatischste Einstieg ist, strictNullChecks global in der tsconfig.json einzuschalten, aber alle noch nicht migrierten Dateien über eine separate, weniger strikte Konfiguration auszunehmen. TypeScript unterstützt das über Projektreferenzen oder über ein zweites tsconfig, das die migrierten Dateien explizit auflistet oder per Glob-Pattern einschließt.
Ein alternativer, in der Praxis oft einfacherer Weg ist ein Kommentar am Dateianfang, der von einem Custom-ESLint-Rule oder einem eigenen Skript ausgewertet wird, um zu tracken, welche Dateien bereits als migriert gelten. Wichtig ist in beiden Fällen, dass neue Dateien ab dem ersten Tag der Migration verpflichtend strict sind, damit die Zahl der zu migrierenden Dateien nicht weiterwächst, während am Bestand gearbeitet wird.
// tsconfig.json -- Basis-Konfiguration mit strictNullChecks global aktiv
{
"compilerOptions": {
"strictNullChecks": true,
"strict": false
},
"include": ["src"]
}
// tsconfig.legacy.json -- für noch nicht migrierte Verzeichnisse
{
"extends": "./tsconfig.json",
"compilerOptions": { "strictNullChecks": false },
"include": ["src/legacy-module-a", "src/legacy-module-b"]
}
3. ts-migrate und ähnliche Werkzeuge für die Erstbehandlung
ts-migrate von Airbnb automatisiert einen großen Teil der Erstbehandlung: Das Tool fügt an jeder Fehlerstelle einen @ts-expect-error-Kommentar mit einer eindeutigen Fehlerreferenz ein, sodass der Build sofort wieder grün wird, ohne dass jemand alle Fehler manuell beheben muss. Das verwandelt eine unlösbare Blockade in eine priorisierbare Liste von TODOs.
Der Haken an diesem Ansatz: @ts-expect-error unterdrückt den Fehler nur, es löst ihn nicht. Ohne einen zweiten, disziplinierten Schritt, der diese Kommentare systematisch wieder entfernt und die tatsächlichen Nullchecks ergänzt, bleibt die Codebasis dauerhaft in einem Halbmigrationszustand mit hunderten stummgeschalteten Fehlern, die niemand mehr anfasst.
# ts-migrate auf ein einzelnes Verzeichnis anwenden
npx ts-migrate migrate src/legacy-module-a
# Anzahl verbleibender @ts-expect-error-Unterdrückungen zählen,
# um den Migrationsfortschritt zu tracken
grep -r "@ts-expect-error" src/legacy-module-a --include="*.ts" | wc -l
4. Die vier häufigsten Fehlerklassen und wie man sie behebt
Die meisten Fehler nach dem Einschalten von strictNullChecks lassen sich in wenige Muster einordnen: fehlende Null-Checks vor Property-Zugriffen, Funktionsparameter, die implizit optional waren, Array-Zugriffe per Index, die TypeScript standardmäßig als garantiert nicht-undefiniert behandelt, und veraltete Type-Guards, die null nicht ausschließen.
Für Array-Zugriffe per Index empfiehlt sich zusätzlich die Option noUncheckedIndexedAccess, die arr[i] korrekt als T | undefined statt als T typisiert, ein Fehler, den strictNullChecks allein nicht abdeckt, weil TypeScript aus Kompatibilitätsgründen Indexzugriffe standardmäßig als sicher annimmt.
// Fehlerklasse 1: fehlender Null-Check vor Property-Zugriff
function getCity(user: User | null): string {
return user.address.city; // Fehler: user könnte null sein
}
// Fix:
function getCityFixed(user: User | null): string {
if (user === null) throw new Error("Kein Nutzer");
return user.address.city;
}
// Fehlerklasse 2: impliziter optionaler Parameter
function greet(name: string = null) {} // Fehler seit strictNullChecks
function greetFixed(name: string | null = null) {}
// Fehlerklasse 3: Array-Index ohne noUncheckedIndexedAccess
const first = users[0].name; // ohne die Option "sicher", ist es aber nicht
5. Der Non-Null-Assertion-Operator als kontrolliertes Ventil
Der !-Operator unterdrückt eine Null-Warnung, ohne eine echte Laufzeitprüfung einzufügen, was ihn zu einem gefährlichen Werkzeug macht, wenn er unreflektiert überall dort eingesetzt wird, wo der Compiler meckert. Während einer Migration ist die Versuchung groß, jeden Fehler per ! stummzuschalten, was die Migration zwar formal abschließt, aber die eigentliche Absicherung gegen Laufzeitfehler zunichtemacht.
Ein sinnvoller Kompromiss ist, ! nur an Stellen zu erlauben, an denen die Nicht-Null-Garantie durch Kontext eindeutig belegbar ist, etwa direkt nach einer vorangegangenen Existenzprüfung, und ansonsten echte Guards oder Default-Werte zu bevorzugen. Ein Lint-Regel, die die Anzahl neuer !-Vorkommen pro Pull-Request begrenzt, hilft, diese Disziplin durchzusetzen.
6. Module priorisieren: wo zuerst migrieren
Nicht alle Module sind gleich wichtig für eine Migration. Kern-Domänenlogik mit vielen internen Konsumenten und hoher Änderungsfrequenz profitiert am meisten von echter Nullsicherheit, weil dort Bugs den größten Schaden anrichten und die häufigsten Codeänderungen stattfinden. Reine UI-Komponenten mit wenig Businesslogik lassen sich oft bis zuletzt zurückstellen.
Ein pragmatisches Priorisierungskriterium ist die Kombination aus Fehleranzahl pro Datei und Änderungsfrequenz aus der Git-Historie: Dateien mit wenigen Fehlern und hoher Änderungsrate sind günstige erste Kandidaten, weil sie schnell migriert sind und danach jede weitere Änderung bereits im sicheren Modus stattfindet.
7. Ein CI-Gate gegen Rückschritte
Ohne technische Absicherung schleicht sich leicht wieder neuer, nicht-strikter Code in bereits migrierte Bereiche ein, insbesondere wenn mehrere Entwickler parallel arbeiten. Ein CI-Schritt, der die Liste der migrierten Dateien mit der tatsächlichen Fehlerzahl bei aktivem strictNullChecks abgleicht, verhindert, dass die Fehlerzahl in bereits abgehakten Bereichen unbemerkt wieder steigt.
Ein einfaches Skript zählt die Fehler pro Datei bei erzwungenem strictNullChecks und vergleicht das Ergebnis mit einer eingecheckten Baseline-Datei, ein Anstieg bricht den Build ab, ein Rückgang aktualisiert automatisch die Baseline. So wird Fortschritt messbar und Rückschritt technisch unmöglich.
# Vereinfachtes CI-Gate-Skript
tsc --strictNullChecks --noEmit 2> errors.txt
CURRENT=$(wc -l < errors.txt)
BASELINE=$(cat null-check-baseline.txt)
if [ "$CURRENT" -gt "$BASELINE" ]; then
echo "Neue strictNullChecks-Fehler eingeführt: $CURRENT > $BASELINE"
exit 1
fi
8. Team-Koordination während einer länger laufenden Migration
Eine Migration, die sich über mehrere Wochen oder Monate erstreckt, braucht Transparenz: Ein zentrales Dashboard oder eine regelmäßig aktualisierte Liste, welche Module bereits strict sind, verhindert doppelte Arbeit und macht Fortschritt sichtbar. Ohne diese Sichtbarkeit verliert sich Migrationsarbeit erfahrungsgemäß schnell in konkurrierenden Prioritäten.
Es lohnt sich, die Migration in kleine, täglich abschließbare Einheiten zu zerlegen, etwa eine einzelne Datei oder ein kleines Feature-Modul pro Pull-Request, statt große Verzeichnisse in einem Rutsch zu migrieren. Kleinere Pull-Requests sind leichter zu reviewen und reduzieren das Risiko, dass ein Merge-Konflikt eine halb fertige Migration blockiert.
Ein kurzer wöchentlicher Statusbericht, der den aktuellen Fehlerstand gegen die Baseline stellt und die zuletzt migrierten Module auflistet, hält die Motivation im Team hoch und macht sichtbar, dass die Arbeit tatsächlich vorankommt, auch wenn einzelne Wochen wenig sichtbaren Fortschritt bringen.
9. Realistische Zeitplanung und Erfolgskriterien
Die Dauer einer strictNullChecks-Migration hängt stark von der Codebasisgröße und dem ursprünglichen Disziplinlevel ab, realistisch sind für mittelgroße Projekte mehrere Wochen bei kontinuierlicher, aber nicht vollzeitiger Arbeit. Wichtiger als ein festes Datum ist ein sinkender Trend in der Fehlerzahl und ein CI-Gate, das verhindert, dass neuer Code das Problem vergrößert.
Am Ende zählt nicht nur, dass der Compiler keine Fehler mehr meldet, sondern dass die verbleibenden Non-Null-Assertions bewusst gesetzt und dokumentiert sind, statt als Fehler-Unterdrückung aus der Migrationsphase liegen geblieben zu sein.
Ein oft unterschätzter Nebeneffekt einer abgeschlossenen Migration ist, wie stark sie die Einarbeitungszeit neuer Teammitglieder verkürzt: Wer den Code liest, muss nicht mehr an jeder Stelle mental prüfen, ob ein Wert theoretisch null sein könnte, sondern kann sich auf die im Typ sichtbare Garantie verlassen. Dieser Effekt rechtfertigt den Migrationsaufwand oft stärker als die reine Anzahl vermiedener Laufzeitfehler.
| Ansatz | Aufwand | Risiko | Geeignet für |
|---|---|---|---|
| Big-Bang-Umschaltung | sehr hoch, kurzfristig | hoch, Team blockiert | sehr kleine Codebasen |
| ts-migrate mit @ts-expect-error | gering initial, hoch später | mittel bei Nachlässigkeit | schnellen grünen Build erzwingen |
| Ordnerweise Opt-in | moderat, verteilt | gering, kontrollierbar | mittlere bis große Codebasen |
| CI-Gate mit Baseline | gering, einmalig einrichten | sehr gering | jede laufende Migration |
Mironsoft
TypeScript-Migration, Typsicherheit und Team-Onboarding
JavaScript-Codebasis ohne Typsicherheit, aber keine Zeit für eine Rundum-Migration?
Wir migrieren bestehende JavaScript-Projekte schrittweise zu TypeScript, richten strikte Compiler-Einstellungen sauber ein und bringen Teams mit Code-Reviews und Style-Guides auf denselben Typsicherheits-Stand.
Migrations-Fahrplan
Schrittweise JS-zu-TS-Migration ohne Big-Bang-Risiko planen und umsetzen.
Strict-Mode-Einführung
tsconfig.json, ESLint-Regeln und CI-Checks für dauerhafte Typsicherheit aufsetzen.
Team-Onboarding
Entwickler mit Workshops und Code-Reviews in TypeScript-Best-Practices einarbeiten.
10. Zusammenfassung
strictNullChecks-Migration
Strategie
Opt-in per Ordner statt globaler Big-Bang-Umschaltung
Tooling
ts-migrate für @ts-expect-error als erste Entlastung
Absicherung
CI-Gate mit Fehler-Baseline verhindert Rückschritte
Priorisierung
Kern-Domänenlogik mit hoher Änderungsrate zuerst