TypeScript Strict Mode: Warum dein Team ihn aktivieren sollte und wie der Umstieg gelingt

#TypeScript Strict Mode
Sandor Farkas - Founder & Lead Developer at Wolf-Tech

Sandor Farkas

Gründer & Lead Developer

Experte für Softwareentwicklung und Legacy-Code-Optimierung

Eine Dashboard-Komponente geht am Montagmorgen in Produktion. Am Dienstagnachmittag trifft ein Support-Ticket ein: Die Seite stuerzt mit Cannot read properties of undefined (reading 'totalRevenue') ab, sobald ein neu registrierter Nutzer, der das Onboarding nicht abgeschlossen hat, das Dashboard laedt. Der TypeScript-Compiler war zufrieden. Der Typ sagte User.companyMetrics: CompanyMetrics. Die API gibt fuer Nutzer ohne Unternehmen null zurueck, und der Frontend-Code hat das nie geprueft.

Das ist genau die Klasse von Bugs, die der TypeScript Strict Mode verhindern soll, und es ist einer der haeufigsten Gruende, warum Teams, die TypeScript bereits nutzen, weiterhin Laufzeitfehler ausliefern. Den Strict Mode einzuschalten ist keine kosmetische Aenderung. Es ist eine Verpflichtung zu einem strengeren Vertrag zwischen deinen Typen und deiner Laufzeit, und auf einer bestehenden Codebasis bringt er hunderte latenter Bugs ans Licht, die sich still hinter laxen Defaults versteckt haben. Dieser Beitrag erklaert, was der TypeScript Strict Mode tatsaechlich aktiviert, welche Flags den hoechsten Return on Investment liefern und eine schrittweise Einfuehrungsstrategie, die fuer Teams funktioniert, die es sich nicht leisten koennen, die Feature-Entwicklung zwei Wochen lang einzufrieren, waehrend sie 2.000 Compiler-Fehler beheben.

Was der TypeScript Strict Mode tatsaechlich aktiviert

"strict": true in der tsconfig.json zu setzen ist ein Meta-Flag. Es aktiviert acht einzelne Optionen auf einmal, und sie einzeln zu verstehen ist wichtig, weil du sie waehrend einer Migration einzeln aktivieren wirst. Stand TypeScript 5.x sind die Flags: noImplicitAny, strictNullChecks, strictFunctionTypes, strictBindCallApply, strictPropertyInitialization, alwaysStrict, noImplicitThis und useUnknownInCatchVariables.

Zwei davon sind in Bezug auf Fehlervolumen und realen Effekt weit wichtiger als die anderen. strictNullChecks zwingt dazu, null und undefined als eigenstaendige Typen zu behandeln, statt als Werte, die jedem Typ zuweisbar sind, was den obigen Dashboard-Bug abgefangen haette. noImplicitAny zwingt jeden Parameter, jeden Rueckgabewert und jede Variable ohne explizite Annotation und ohne ableitbaren Typ zur Annotation, statt stillschweigend auf any zurueckzufallen. Zusammen sind diese beiden Flags fuer die ueberwaeltigende Mehrheit der Fehler verantwortlich, die du beim ersten Aktivieren des Strict Mode auf einer Legacy-Codebasis siehst, und sie sind auch fuer die meisten Laufzeit-Bugs verantwortlich, die der Strict Mode verhindert.

Zwei weitere Flags verdienen eigenstaendige Aufmerksamkeit. strictPropertyInitialization zwingt Klassen-Eigenschaften dazu, im Konstruktor initialisiert oder als optional deklariert zu werden, und faengt eine subtile Familie von Bugs ab, bei denen auf eine Eigenschaft zugegriffen wird, bevor sie zugewiesen wurde. useUnknownInCatchVariables typisiert abgefangene Fehler als unknown statt any und zwingt den Code dazu, einzugrenzen, bevor er .message oder .stack auf etwas zugreift, das vielleicht gar kein Error ist. Es gibt ausserdem ein separates Flag, exactOptionalPropertyTypes, das nicht durch strict: true aktiviert wird, aber eine spezifische Luecke in der Behandlung optionaler Eigenschaften schliesst: { name?: string } akzeptiert normalerweise auch { name: undefined } und nicht nur {}, was manchmal nicht das ist, was du willst. Aktiviere es erst, nachdem du den Rest stabilisiert hast.

Warum Strict Null Checks das wichtigste Flag ist, das du je aktivieren wirst

Ohne strictNullChecks behandelt TypeScript null und undefined als Untertypen jedes anderen Typs. Eine als string typisierte Variable kann zur Laufzeit legal null enthalten, und der Compiler warnt dich nicht.

// strictNullChecks AUS
interface User {
  email: string;
}

function getEmailDomain(user: User): string {
  return user.email.split('@')[1]; // Stuerzt zur Laufzeit ab, wenn email tatsaechlich null ist
}

Mit aktiviertem strictNullChecks zwingt das Typsystem den Code, den Fall zu behandeln, falls User.email null sein kann:

// strictNullChecks AN
interface User {
  email: string | null;
}

function getEmailDomain(user: User): string {
  if (user.email === null) {
    return '';
  }
  const parts = user.email.split('@');
  return parts[1] ?? '';
}

Ein sekundaerer Effekt ueberrascht Teams oft: Array-Indexierung gibt T | undefined zurueck, sobald auch noUncheckedIndexedAccess aktiviert ist, und selbst ohne dieses Flag gibt Array.prototype.find T | undefined zurueck. Code, der frueher annahm, ein .find()-Ergebnis sei immer vorhanden, muss nun den fehlenden Fall explizit behandeln. Das ist kein Bug in TypeScript, sondern TypeScript, das dem Entwickler endlich von einer Annahme erzaehlt, die still getroffen wurde.

Eine realistische Migrationsstrategie fuer bestehende Codebasen

Der haeufigste Fehler, den Teams beim Aktivieren des Strict Mode machen, ist, das Flag an einem Freitagnachmittag in einem Pull Request umzulegen und am Montag in einem Meer von zweitausend Fehlern zu ertrinken. Eine bessere Strategie ist inkrementell, wobei jeder Schritt eine Codebasis erzeugt, die sauber kompiliert.

Der erste Schritt ist, eine zweite tsconfig zu erstellen (nenne sie tsconfig.strict.json), die die Hauptkonfiguration erweitert und den Strict Mode nur fuer eine Whitelist einer Teilmenge von Dateien aktiviert. Beginne mit brandneuen Dateien und einem kleinen, gut isolierten Modul, das du zuerst haerten willst. Verwende include, um die Dateien unter strikter Pruefung aufzulisten, und fuehre beide Konfigurationen in CI aus: Die Hauptkonfiguration muss die gesamte Codebasis fehlerfrei kompilieren, und die strikte Konfiguration muss die Whitelist-Teilmenge fehlerfrei kompilieren. So haertest du die Codebasis Datei fuer Datei, ohne je den Build zu zerstoeren.

// tsconfig.strict.json
{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true
  },
  "include": [
    "src/domain/**/*.ts",
    "src/lib/payments/**/*.ts"
  ]
}

Der zweite Schritt ist, ein striktes Flag nach dem anderen auf Ebene der gesamten Codebasis zu aktivieren. noImplicitAny ist meist das einfachste: Es faengt fehlende Annotationen ab, foerdert aber selten Logik-Bugs zutage. strictNullChecks ist das schwierigste und wertvollste. Es ueber eine etablierte Next.js-Anwendung zu aktivieren erzeugt typischerweise zwischen 200 und 2.000 Fehler, je nach Codebasis-Groesse und bestehender Disziplin. Behebe die Fehler in Stapeln nach Modul statt nach Fehleranzahl, weil sich Fehler um spezifische Domaenenkonzepte gruppieren (ein User-Objekt, das null sein kann, ein aus der Umgebung gelesener Konfigurationswert, eine Server-Action, deren Rueckgabetyp tatsaechlich einen Fehlerfall enthaelt). Sie gemeinsam zu beheben offenbart oft eine sauberere Form fuer den Domaenentyp.

Der dritte Schritt ist, noUncheckedIndexedAccess und exactOptionalPropertyTypes zu uebernehmen, nachdem das Team mit den grundlegenden Strict-Flags vertraut ist. Diese fangen subtilere Bugs ab, erfordern aber mehr Refactoring, um erfuellt zu werden, und sie zu frueh zu aktivieren erzeugt tendenziell Widerstand, der die gesamte Migration verlangsamt.

Muster fuer den Umgang mit den schwierigsten Fehlern

Die meisten Strict-Mode-Fehler fallen in eine Handvoll Kategorien, jede mit einem gut verstandenen Muster.

Bei Fehlern zu moeglicherweise null-wertigen Werten bevorzuge Type Guards und fruehe Returns gegenueber Non-Null-Assertions. Der !-Operator (user.email!.split('@')[1]) bringt den Compiler zum Schweigen, ohne zur Laufzeit etwas Nuetzliches zu tun: Der Absturz passiert trotzdem, nur die Warnung verschwindet. Reserviere ! fuer Faelle, in denen externes Wissen, das der Compiler nicht sehen kann, garantiert, dass der Wert vorhanden ist, etwa Werte, die bereits frueher im Request-Zyklus von einer Laufzeit-Schema-Bibliothek wie Zod oder Valibot validiert wurden.

Bei Fehlern zu unknown in Catch-Bloecken fuehre einen kleinen Helfer zur Fehlereingrenzung ein:

function getErrorMessage(error: unknown): string {
  if (error instanceof Error) return error.message;
  if (typeof error === 'string') return error;
  return 'Unknown error';
}

try {
  await processPayment(orderId);
} catch (error) {
  logger.error('Payment failed', { message: getErrorMessage(error), orderId });
}

Bei Fehlern zu nicht initialisierten Klassen-Eigenschaften ist die richtige Loesung fast immer, die Eigenschaft im Konstruktor zu initialisieren, sie mit ? als optional zu markieren oder die Definite-Assignment-Assertion ! fuer Eigenschaften zu verwenden, die tatsaechlich von einem Framework initialisiert werden (Angulars Dependency Injection, TypeORM-Relationen). Vermeide das verlockende Muster, Eigenschaften Default-Werte zu geben, die keinen gueltigen Zustand darstellen. Ein leerer String fuer eine User-ID etwa erzeugt stille Bugs weiter unten, wenn Code annimmt, jeder nicht-leere Wert, den er sieht, sei echt.

Bei grossen Refactorings in React- und Next.js-Codebasen eliminieren Discriminated Unions ganze Kategorien von Null-Check-Fehlern auf Typebene. Eine Komponente, die entweder einen Lade-, einen Fehler- oder einen Datenzustand anzeigt, wird viel sauberer, wenn ihre Props erzwingen, dass genau einer dieser drei Zustaende existiert:

type DashboardState =
  | { status: 'loading' }
  | { status: 'error'; message: string }
  | { status: 'success'; data: DashboardData };

function Dashboard({ state }: { state: DashboardState }) {
  if (state.status === 'loading') return <Spinner />;
  if (state.status === 'error') return <ErrorBanner message={state.message} />;
  return <Panel data={state.data} />;
}

Der Compiler garantiert nun, dass auf state.data nur im Success-Zweig und auf state.message nur im Error-Zweig zugegriffen wird. Keine Laufzeitpruefungen, kein Optional Chaining, keine defensiven Defaults: Die Typen haben die Arbeit erledigt.

Den Strict Mode anlassen: Regression verhindern

Sobald der Strict Mode aktiviert ist, ist die schwierigere Aufgabe, ihn anzulassen. Zwei Gewohnheiten verhindern Regression. Erstens: Erzwinge die strikte Konfiguration in CI mit einem separaten Build-Schritt, der den Pull Request scheitern laesst, falls neue oder geaenderte Dateien strikte Fehler einfuehren. Das ist wichtig, weil ein Code-Review allein nicht zuverlaessig ist, um ein wieder eingefuehrtes any oder einen fehlenden Null-Check abzufangen. Zweitens: Fuege ESLint-Regeln (@typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion) auf Schweregrad warn oder error hinzu, um Notausgaenge im Review sichtbar zu halten statt verborgen.

Teams mit einer wachsenden Codebasis profitieren auch davon, die Strict-Abdeckung explizit zu verfolgen. Ein kleines Skript, das die in der strikten Konfiguration enthaltenen Dateien zaehlt und den Prozentsatz der Codebasis unter strikter Pruefung meldet, laesst sich zu einem Dashboard hinzufuegen. Fortschritt wird sichtbar und motivierend statt abstrakt, was fuer Migrationen, die mehrere Monate dauern, sehr wichtig ist.

Der Ertrag

Der haeufigste Widerstand gegen das Aktivieren des TypeScript Strict Mode ist, dass er die Feature-Arbeit kurzfristig verlangsamt. Nach unserer Erfahrung mit Code-Quality-Audits an Next.js- und Node.js-Codebasen ist der Produktivitaetseinbruch real, dauert aber zwei bis sechs Wochen, und Teams erholen sich schnell. Dauerhaft bleibt eine ganze Kategorie null-bezogener Bugs, die nicht mehr in Produktion gelangen, eine Codebasis, in der Refactoring sicherer ist, weil die Typen das Laufzeitverhalten ehrlich beschreiben, und neue Entwickler, die sich einarbeiten koennen, indem sie die Typen lesen, statt die App auszufuehren und auf Fehler zu warten.

Fuer Teams, die eine Legacy-TypeScript-Codebasis erben, die vor dem Standardwerden von Strict Mode oder Strict Null Checks geschrieben wurde, ist diese Migration eine der wirkungsvollsten verfuegbaren Investitionen. Der Code wird sicherer, die Typen werden ehrlich, und die Laufzeitfehler, die Nutzer frustrieren und Support-Kapazitaet verbrauchen, beginnen zu verschwinden. Wenn dein Team diesen Umstieg erwaegt oder mit einer Codebasis ringt, in der Typen und Laufzeit auseinandergedriftet sind, helfen wir europaeischen Teams, den Strict Mode auf produktiven Next.js- und Node.js-Anwendungen einzufuehren, ohne die Feature-Entwicklung einzufrieren. Kontaktiere uns unter hello@wolf-tech.io oder besuche wolf-tech.io fuer eine kostenlose Beratung.