Vom laxen any zur typsicheren Codebasis
Der strict Modus in TypeScript ist kein einzelner Schalter, sondern ein Bündel aus acht Compiler-Flags, die gemeinsam über echte Typsicherheit entscheiden. Wer strict erst nach Jahren aktiviert, sieht oft tausende Fehler auf einmal und bricht die Migration ab. Dieser Artikel zeigt, welche Flags den größten Nutzen bei geringstem Aufwand bringen und wie eine Bestandscodebasis realistisch schrittweise umgestellt wird.
Inhaltsverzeichnis
- 1. Was der strict-Modus in tsconfig.json wirklich einschaltet
- 2. Die einzelnen Strict-Flags im Detail
- 3. Warum strict Mode spät im Projekt richtig wehtut
- 4. Schrittweise Migration: der pragmatische Weg für Bestandscode
- 5. tsconfig-Strategien: per Verzeichnis und Projekt-Referenzen
- 6. strictNullChecks in der Praxis: ein Vorher-Nachher
- 7. CI-Gating: neue Dateien strikt, Legacy-Code geduldet
- 8. Editor-Tooling und Team-Workflow während der Migration
- 9. Strict-Flags im direkten Vergleich: Aufwand gegen Nutzen
- 10. Zusammenfassung
- 11. FAQ
1. Was der strict-Modus in tsconfig.json wirklich einschaltet
strict ist in tsconfig.json kein einzelnes Flag, sondern ein Sammelschalter für aktuell acht einzelne Compiler-Optionen: alwaysStrict, noImplicitAny, noImplicitThis, strictBindCallApply, strictFunctionTypes, strictNullChecks, strictPropertyInitialization und useUnknownInCatchVariables. Jede dieser Optionen schaltet für sich eine zusätzliche Prüfung ein, die im laxen Modus stillschweigend übersprungen wird. Wichtig für die Praxis: noUncheckedIndexedAccess, exactOptionalPropertyTypes und noImplicitOverride gehören trotz ähnlichem Charakter nicht zu strict, sondern müssen einzeln aktiviert werden.
Wer "strict": true setzt statt die Flags einzeln zu listen, bekommt bei jedem TypeScript-Update automatisch jede neue Prüfung, die das TypeScript-Team künftig zur strict-Familie hinzufügt. Das ist in der Regel gewünscht, kann aber ein Minor-Version-Update der Abhängigkeit typescript in der CI-Pipeline unerwartet scheitern lassen, weil plötzlich neue Fehler auftauchen. Teams mit hoher Stabilitätsanforderung pinnen deshalb die TypeScript-Version exakt und reviewen Compiler-Updates bewusst, statt sie über einen Caret-Range automatisch einzuspielen.
2. Die einzelnen Strict-Flags im Detail
noImplicitAny verbietet Parameter, Variablen und Rückgabewerte ohne erkennbaren Typ und ist meist das Flag mit der größten Fehlerzahl beim ersten Aktivieren, weil jede fehlende Annotation sichtbar wird. strictNullChecks trennt null und undefined von allen anderen Typen und zwingt Entwickler, jeden potenziell leeren Wert explizit zu behandeln, bevor darauf zugegriffen wird. strictFunctionTypes prüft Parametertypen von Funktionen kontravariant, was unsichere Callback-Zuweisungen aufdeckt, die im laxen Modus klaglos kompilieren.
strictPropertyInitialization verlangt, dass jede Klasseneigenschaft entweder im Konstruktor gesetzt oder explizit als undefined typisiert wird, was klassische this.value-Bugs in Services und Repositories verhindert. strictBindCallApply prüft Argumente von bind, call und apply gegen die Original-Signatur der Funktion. noImplicitThis markiert this-Zugriffe mit unklarem Kontext, und useUnknownInCatchVariables typisiert Catch-Variablen seit TypeScript 4.4 als unknown statt any, was ungeprüfte Fehlerzugriffe wie err.message ohne vorherige Typprüfung verhindert.
// tsconfig.json - the "strict" flag family, spelled out explicitly
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Bundler",
// Equivalent to "strict": true - listed individually for clarity
"alwaysStrict": true,
"noImplicitAny": true,
"noImplicitThis": true,
"strictBindCallApply": true,
"strictFunctionTypes": true,
"strictNullChecks": true,
"strictPropertyInitialization": true,
"useUnknownInCatchVariables": true,
// NOT included in "strict" - opt in separately for extra safety
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": false,
"noImplicitOverride": true
}
}
3. Warum strict Mode spät im Projekt richtig wehtut
Je länger eine Codebasis ohne strict wächst, desto mehr implizite Annahmen verstecken sich in Funktionssignaturen, die niemand mehr hinterfragt. Eine Funktion, die seit drei Jahren any zurückgibt, wird an zwanzig Stellen im Projekt aufgerufen, jede davon mit eigenen impliziten Annahmen über die tatsächliche Form der Daten. Wird strict nachträglich aktiviert, meldet der Compiler nicht zwanzig unabhängige Fehler, sondern eine Fehlerkaskade, bei der jede Korrektur neue Folgefehler an verwandten Stellen aufdeckt.
Der psychologische Effekt ist real: Ein Pull-Request mit 4000 neuen TypeScript-Fehlern wirkt für ein Team demotivierend und wird in der Praxis fast immer verworfen oder auf unbestimmte Zeit verschoben. Genau deshalb scheitern Big-Bang-Migrationen so zuverlässig. Der wirtschaftliche Schaden ist real, aber unsichtbar: Jeder Monat ohne strictNullChecks produziert neue null-unsichere Stellen, die später ebenfalls migriert werden müssen. Der richtige Zeitpunkt für strict ist der Projektstart. Der zweitbeste Zeitpunkt ist heute, mit einem realistischen, schrittweisen Plan statt eines einzelnen riesigen Refactorings.
4. Schrittweise Migration: der pragmatische Weg für Bestandscode
Statt strict global umzuschalten, aktiviert man die Flags einzeln und beginnt mit den günstigsten: strictBindCallApply, noImplicitThis und strictPropertyInitialization verursachen in den meisten Codebasen nur eine überschaubare Fehleranzahl. noImplicitAny und strictNullChecks folgen danach, im Idealfall Datei für Datei statt projektweit. Die Direktive // @ts-check am Dateianfang erlaubt es sogar, einzelne .js-Dateien vorab typzuprüfen, bevor das gesamte Projekt auf .ts und strict umgestellt ist.
Für Stellen, die sich nicht sofort sauber lösen lassen, ist // @ts-expect-error dem globalen // @ts-ignore vorzuziehen: Es unterdrückt den aktuellen Fehler, schlägt aber selbst fehl, sobald der Fehler behoben wurde und die Annotation überflüssig ist. So bleibt eine Migrationsliste automatisch aktuell, ohne dass vergessene Ignores unbemerkt liegen bleiben. Ein wöchentliches CI-Reporting der verbleibenden @ts-expect-error-Kommentare macht den Fortschritt sichtbar und motiviert das Team messbar.
// @ts-check
// legacy-pricing.js - incremental adoption before converting to .ts
/**
* @param {{ price: number, discountPercent?: number }} item
* @returns {number}
*/
function calculateFinalPrice(item) {
// @ts-expect-error - legacy callers still pass a string here, tracked in TICKET-482
const discount = item.discountPercent ?? "0";
return item.price - (item.price * Number(discount)) / 100;
}
// Once every caller passes a real number, remove the @ts-expect-error
// line above - the build fails automatically if it becomes unused,
// which is exactly the signal that this call site is ready to migrate.
5. tsconfig-Strategien: per Verzeichnis und Projekt-Referenzen
Eine bewährte Struktur für gemischte Codebasen trennt strikten neuen Code von lockerem Bestandscode über zwei tsconfig.json-Dateien statt einer einzigen globalen Konfiguration. Ein tsconfig.base.json definiert gemeinsame Compiler-Optionen wie target und module, während tsconfig.strict.json zusätzlich strict: true setzt und ausschließlich neue Verzeichnisse wie src/modules/ einschließt. Legacy-Code unter src/legacy/ bleibt vorerst auf der lockeren Basis-Konfiguration, ohne dass ein einziger Build-Schritt beide Welten vermischt.
Project References ("references": [...] mit composite: true) gehen einen Schritt weiter und erlauben inkrementelle Builds pro Teilprojekt, was gerade in Monorepos die Build-Zeit spürbar reduziert, weil tsc --build nur geänderte Referenzen neu kompiliert. Für kleinere Projekte reicht meist ein einfacheres Muster: zwei include-Globs in getrennten Konfigurationsdateien, die im package.json-Skript nacheinander mit tsc -p tsconfig.strict.json --noEmit und tsc -p tsconfig.legacy.json --noEmit geprüft werden.
// tsconfig.strict.json - strict rules apply only to new modules
{
"extends": "./tsconfig.base.json",
"compilerOptions": {
"strict": true,
"composite": true,
"outDir": "./dist/modules"
},
"include": ["src/modules/**/*.ts"]
}
// tsconfig.legacy.json - existing code stays on loose settings for now
{
"extends": "./tsconfig.base.json",
"compilerOptions": {
"strict": false,
"noImplicitAny": false,
"outDir": "./dist/legacy"
},
"include": ["src/legacy/**/*.ts"]
}
6. strictNullChecks in der Praxis: ein Vorher-Nachher
Ohne strictNullChecks ist null und undefined jedem anderen Typ zuweisbar, wodurch der Compiler eine der häufigsten Laufzeitfehlerquellen komplett übersieht: den Zugriff auf eine Eigenschaft eines Werts, der zur Laufzeit gar nicht existiert. Genau dieses Muster erzeugt die berüchtigte Cannot read properties of undefined-Exception in Produktion, oft erst Wochen nach dem Deployment, wenn ein bestimmter Datensatz zum ersten Mal ein leeres Feld enthält.
Mit aktiviertem strictNullChecks verweigert der Compiler genau diesen Zugriff, solange kein expliziter Null-Check, keine optionale Verkettung mit ?. oder kein Type Guard den Fall abdeckt. Der Migrationsaufwand konzentriert sich fast immer auf denselben Mustertyp: Funktionen, die undefined zurückgeben können, aber im Aufrufer als garantiert vorhanden behandelt wurden. Sobald diese Stellen einmal korrigiert sind, findet der Compiler bei jeder künftigen Änderung automatisch neue null-unsichere Zugriffe, ohne dass ein Reviewer das manuell prüfen muss.
// BEFORE: compiles fine without strictNullChecks, crashes at runtime
function getPrimaryAddress(customer) {
const address = customer.addresses.find(a => a.isPrimary);
return address.city.toUpperCase(); // throws if no primary address exists
}
// AFTER: strictNullChecks forces the missing case to be handled explicitly
interface Address {
city: string;
isPrimary: boolean;
}
interface Customer {
addresses: Address[];
}
function getPrimaryAddress(customer: Customer): string {
const address = customer.addresses.find((a) => a.isPrimary);
if (!address) {
throw new Error("Customer has no primary address");
}
return address.city.toUpperCase();
}
7. CI-Gating: neue Dateien strikt, Legacy-Code geduldet
Ein globales strict: true im gesamten Repository ist selten sofort erreichbar, aber eine CI-Regel, die nur neue oder geänderte Dateien strikt prüft, lässt sich meist innerhalb eines Tages einführen. Das Prinzip: git diff ermittelt die geänderten .ts-Dateien gegenüber dem Zielbranch, und nur diese Dateien werden gegen die strikte Konfiguration geprüft, während der Rest des Repositories weiterhin gegen die lockere Basis-Konfiguration kompiliert. So verhindert die Pipeline zuverlässig neue Verstöße, ohne bestehenden Code sofort zu blockieren.
Wichtig ist, diese Regel nicht als Ehrensystem zu behandeln, sondern als harten CI-Gate: Ein Pull-Request, der eine neue Datei ohne Typannotationen hinzufügt, darf nicht mergen. Gleichzeitig sollte die Anzahl der verbleibenden Legacy-Fehler in einer Baseline-Datei versioniert werden, damit die Pipeline zusätzlich fehlschlägt, wenn diese Zahl unerwartet steigt statt zu sinken. Dieses Muster, bekannt als Ratchet, verhindert stille Rückschritte, ohne eine vollständige Migration zur Voraussetzung für jeden einzelnen Merge zu machen.
#!/usr/bin/env bash
# ci-strict-gate.sh - type-check only files changed vs. the target branch
set -euo pipefail
TARGET_BRANCH="${TARGET_BRANCH:-main}"
# Collect changed .ts files, excluding deletions
mapfile -t changed_files < <(git diff --name-only --diff-filter=d "origin/${TARGET_BRANCH}...HEAD" -- '*.ts' '*.tsx')
if [[ ${#changed_files[@]} -eq 0 ]]; then
echo "No TypeScript files changed, skipping strict gate."
exit 0
fi
echo "Type-checking ${#changed_files[@]} changed file(s) under strict mode:"
printf ' %s\n' "${changed_files[@]}"
# Run the strict project config, but only report errors in changed files
npx tsc -p tsconfig.strict.json --noEmit | grep -F "${changed_files[@]/#/}" && exit 1
echo "Strict gate passed."
8. Editor-Tooling und Team-Workflow während der Migration
Eine mehrmonatige Migration lebt oder stirbt mit der Sichtbarkeit des Fortschritts. VS Code und andere Language-Server-Clients zeigen Typfehler bereits inline im Editor, bevor tsc überhaupt manuell aufgerufen wird, was Entwicklern erlaubt, neue Verstöße sofort zu beheben statt sie erst im Pull-Request-Review zu entdecken. Ein per-Verzeichnis-tsconfig.json wie im vorherigen Abschnitt sorgt dafür, dass der Editor für neue Module automatisch die strikte Konfiguration lädt, ohne dass Entwickler manuell zwischen Projekten wechseln müssen.
ESLint-Regeln wie @typescript-eslint/no-explicit-any ergänzen den Compiler, indem sie explizite any-Annotationen markieren, die zwar unter noImplicitAny erlaubt sind, aber dieselbe Typunsicherheit bewusst wieder einführen. Ein wöchentliches Team-Ritual, in dem die Anzahl verbleibender @ts-expect-error-Kommentare und deaktivierter Regeln als Kennzahl besprochen wird, hält die Migration als sichtbares, gemeinsames Ziel im Bewusstsein des Teams, statt sie zu einem Nebenprojekt einzelner Entwickler verkommen zu lassen.
9. Strict-Flags im direkten Vergleich: Aufwand gegen Nutzen
Nicht jedes Flag der strict-Familie verursacht denselben Migrationsaufwand, und nicht jedes bringt denselben Sicherheitsgewinn. Die folgende Übersicht ordnet die fünf wichtigsten Flags danach ein, was ohne sie schiefgehen kann, was sie konkret erzwingen und wie hoch der realistische Migrationsaufwand in einer typischen Bestandscodebasis ausfällt.
| Flag | Ohne Flag: Risiko | Mit Flag: Sicherheit | Migrationsaufwand |
|---|---|---|---|
| noImplicitAny | any durchsickert unbemerkt, keine Typprüfung | Jeder Parameter braucht einen erkennbaren Typ | Mittel |
| strictNullChecks | null/undefined crashen erst zur Laufzeit | Compiler erzwingt Null-Checks vor Zugriff | Hoch |
| strictFunctionTypes | Unsichere Callback-Zuweisungen kompilieren | Parametertypen werden kontravariant geprüft | Niedrig |
| strictPropertyInitialization | Properties bleiben undefined ohne Warnung | Compiler erzwingt Initialisierung im Konstruktor | Niedrig |
| strictBindCallApply | bind/call/apply ohne Prüfung der Argumente | Argumente werden gegen Original-Signatur geprüft | Niedrig |
Die Tabelle zeigt ein klares Muster: Die drei Flags mit dem niedrigsten Migrationsaufwand lassen sich in den meisten Codebasen an einem einzigen Nachmittag aktivieren und liefern sofortigen Nutzen. noImplicitAny und strictNullChecks brauchen dagegen einen echten Plan, weil sie tief in bestehende Funktionssignaturen eingreifen. Wer trotzdem klein anfangen will, aktiviert zuerst die drei günstigen Flags, sammelt Team-Vertrauen in den Prozess und geht anschließend mit einem realistischen Zeitplan an die beiden aufwendigeren Flags heran.
Mironsoft
TypeScript-Tooling, Build-Skripte und Headless-Integrationen für Magento-Shops
Bestehende Codebasis sicher auf strict Mode migrieren?
Wir analysieren eure TypeScript-Codebasis, identifizieren die Flags mit dem größten Sicherheitsgewinn und planen eine schrittweise Migration, die euer Team nicht wochenlang blockiert - von tsconfig-Strategie bis CI-Gating für neue Dateien.
Strict-Audit
Analyse eurer tsconfig.json und Priorisierung der Flags nach Aufwand und Nutzen
Migrationsplan
Schrittweise Aktivierung pro Verzeichnis ohne Big-Bang-Refactoring
CI-Integration
Strict-Gating für neue Dateien, Legacy-Code bleibt vorerst unangetastet
10. Zusammenfassung
Der strict Modus in TypeScript löst ein Kernproblem: Implizite Annahmen über Typen, Nullwerte und Funktionssignaturen werden zu expliziten, vom Compiler geprüften Verträgen. noImplicitAny und strictNullChecks liefern den größten Sicherheitsgewinn, verursachen aber auch den größten Migrationsaufwand in Bestandscode. strictFunctionTypes, strictPropertyInitialization und strictBindCallApply lassen sich dagegen meist innerhalb weniger Stunden aktivieren und liefern sofortigen Nutzen ohne große Refactoring-Arbeit.
Der entscheidende Unterschied zwischen erfolgreichen und gescheiterten Migrationen liegt selten in der technischen Schwierigkeit der einzelnen Flags, sondern in der Vermeidung von Big-Bang-Refactorings. Per-Verzeichnis-tsconfig.json-Dateien, // @ts-expect-error als nachverfolgbare Übergangslösung und ein CI-Gate für neue Dateien erlauben es, strict Mode schrittweise einzuführen, ohne bestehenden Code sofort blockieren zu müssen. So bleibt die Migration ein kontinuierlicher Prozess statt eines einmaligen, riskanten Großprojekts.
Strict Mode in TypeScript - Das Wichtigste auf einen Blick
noImplicitAny & strictNullChecks zuerst
Größter Sicherheitsgewinn bei überschaubarem Aufwand, deckt die häufigsten Laufzeitfehler ab.
Migration pro Verzeichnis
Project References und per-Ordner tsconfig.json vermeiden Big-Bang-Migrationen.
CI-Gating statt Zwang
Neue Dateien strikt, Legacy-Code über Baseline vorübergehend ausgenommen.
Kein Ersatz für Tests
strict Mode findet Typfehler, keine Logikfehler. Tests bleiben Pflicht.