Strict Mode in TypeScript: Welche Flags wirklich wichtig sind
<T>
type
TypeScript · Strict Mode · tsconfig · Type Safety
Strict Mode in TypeScript: Welche Flags wirklich wichtig sind
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.

14 Min. Lesezeit strictNullChecks · noImplicitAny · strictFunctionTypes TypeScript 5.x · tsconfig.json · CI/CD

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.

11. FAQ: Strict Mode in TypeScript

1Was schaltet der strict Modus in TypeScript genau ein?
Acht einzelne Flags: alwaysStrict, noImplicitAny, noImplicitThis, strictBindCallApply, strictFunctionTypes, strictNullChecks, strictPropertyInitialization und useUnknownInCatchVariables.
2Welches einzelne Flag hat den größten Effekt auf die Codequalität?
strictNullChecks, weil es die häufigste Laufzeitfehlerquelle abdeckt: Zugriff auf Eigenschaften eines Werts, der null oder undefined sein kann. Migrationsaufwand ist entsprechend hoch.
3Warum ist strictNullChecks das schmerzhafteste Flag bei später Aktivierung?
Ohne das Flag sind null/undefined jedem Typ zuweisbar. Gewachsener Code enthält dadurch unzählige implizite Annahmen, die beim Aktivieren gleichzeitig als Fehlerkaskade sichtbar werden.
4Kann ich strict Mode nur für neue Dateien aktivieren?
Ja, über eine separate tsconfig.strict.json mit include nur für neue Verzeichnisse. Ein CI-Gate stellt zusätzlich sicher, dass neue Dateien immer typsicher bleiben.
5Was ist der Unterschied zwischen noImplicitAny und strict: true?
noImplicitAny ist eines von acht Flags in strict: true. Allein verbietet es nur fehlende Typannotationen, deckt aber weder Null-Sicherheit noch Funktionsvarianz ab.
6Wie migriere ich eine große Bestandscodebasis realistisch zu strict Mode?
Flags einzeln aktivieren, mit den günstigsten beginnen, per-Verzeichnis-tsconfig für neue Module nutzen, @ts-expect-error als Übergangslösung und ein CI-Gate gegen neue Verstöße.
7Ist noUncheckedIndexedAccess Teil von strict: true?
Nein. noUncheckedIndexedAccess, exactOptionalPropertyTypes und noImplicitOverride sind eigenständige Flags und müssen separat aktiviert werden.
8Was macht strictFunctionTypes konkret anders?
Prüft Parametertypen kontravariant statt bivariant und erkennt so unsichere Callback-Zuweisungen mit zu breitem Parametertyp.
9Wie verhindere ich, dass Legacy-Dateien die CI-Pipeline blockieren?
Über eine Baseline-Datei mit der aktuellen Fehleranzahl. Die Pipeline schlägt nur bei steigender Zahl fehl - ein Ratchet-Mechanismus gegen Rückschritte.
10Lohnt sich strict Mode auch in kleinen Projekten?
Besonders dort, weil der Aufwand gering ist und implizite Annahmen sich gar nicht erst ansammeln. Für neue Projekte ist strict: true von Tag eins Standard.