Bestehende Codebasis auf strictNullChecks migrieren: Strategie und Stolperfallen
AI generated
type
TypeScript · Migration · Codequalität
strictNullChecks nachträglich einführen
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.

11 Min. Lesezeit strictNullChecks Migration tsconfig

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

11. FAQ: strictNullChecks-Migration

1Kann ich strictNullChecks nur für neue Dateien aktivieren?
Ja, über eine zweite tsconfig für Legacy-Verzeichnisse oder über Projektreferenzen lassen sich neue Dateien verpflichtend strikt halten, während bestehender Code schrittweise nachzieht.
2Löst ts-migrate die Nullchecks automatisch?
Nein, ts-migrate unterdrückt Fehler mit @ts-expect-error, um den Build wieder grün zu machen, die eigentlichen Nullchecks müssen in einem zweiten Schritt manuell nachgetragen werden.
3Wie verhindere ich, dass @ts-expect-error dauerhaft liegen bleibt?
Am besten mit einem Ticket pro Datei oder Modul und einem CI-Report, der die Anzahl verbleibender Unterdrückungen zählt und sichtbar macht, sodass sie nicht in Vergessenheit geraten.
4Ist der Non-Null-Assertion-Operator während der Migration erlaubt?
In Massen ja, aber nur an Stellen, an denen die Nicht-Null-Garantie durch den umgebenden Code eindeutig belegbar ist, nicht als pauschale Methode, jede Fehlermeldung stummzuschalten.
5Welche Module sollte ich zuerst migrieren?
Kern-Domänenlogik mit hoher Änderungsfrequenz und wenigen initialen Fehlern liefert das beste Verhältnis aus Aufwand und Sicherheitsgewinn, reine UI-Komponenten lassen sich meist zurückstellen.
6Brauche ich noUncheckedIndexedAccess zusätzlich zu strictNullChecks?
Für vollständige Sicherheit ja, strictNullChecks allein behandelt Array-Zugriffe per Index standardmäßig weiterhin als garantiert vorhanden, was bei leeren oder kürzeren Arrays zu Laufzeitfehlern führen kann.
7Wie lange dauert eine typische Migration?
Für mittelgroße Projekte realistisch mehrere Wochen bei kontinuierlicher, aber nicht vollzeitiger Arbeit, abhängig von Codebasisgröße und der Anzahl der betroffenen Stellen.
8Wie stelle ich sicher, dass migrierte Bereiche nicht wieder unsicherer werden?
Mit einem CI-Gate, das die Fehleranzahl bei erzwungenem strictNullChecks gegen eine eingecheckte Baseline prüft und den Build bei einem Anstieg abbricht.
9Sollte die gesamte Migration in einem einzigen Pull-Request passieren?
Nein, kleine, täglich abschließbare Einheiten pro Pull-Request lassen sich leichter reviewen und reduzieren das Risiko von blockierenden Merge-Konflikten.
10Was mache ich mit Drittanbieter-Typdefinitionen ohne Nullsicherheit?
Wo eine Bibliothek unpräzise Typen liefert, hilft ein lokales Type-Override in einer .d.ts-Datei oder ein expliziter Wrapper, der die Rückgabe korrekt als T | undefined typisiert.