Strict Mode in TypeScript: Was wird aktiviert

1. Übersicht

"strict": true ist die wichtigste Zeile in tsconfig.json. Aber viele Leute aktivieren sie, ohne zu wissen, was genau damit aktiviert wird. Dieser Artikel geht durch jede Flag, die strict aktiviert, zeigt die tatsächlichen Fehler, die jede Flag abfängt, und einige Flags, die zusätzlich aktiviert werden sollten. Am Ende gibt es einen Weg, um ein älteres Projekt zu strict zu migrieren, ohne Tausende Fehler auf einmal zu beheben.

2. strict ist eine Gruppe von Flags

strict ist nicht eine einzelne Überprüfung. Es ist ein Master-Switch, der gleichzeitig eine Gruppe von Flags aktiviert:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "strict": true
  }
}

Mit TypeScript 5.x und 6.x ist strict: true gleichbedeutend mit:

{
  "compilerOptions": {
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "strictBindCallApply": true,
    "strictPropertyInitialization": true,
    "strictBuiltinIteratorReturn": true,
    "noImplicitThis": true,
    "useUnknownInCatchVariables": true,
    "alwaysStrict": true
  }
}

Es gibt zwei Dinge, die du beachten solltest:

  • Jedes Mal, wenn TypeScript eine neue “strict”-Überprüfung hinzufügt, z.B. useUnknownInCatchVariables in 4.4 oder strictBuiltinIteratorReturn in 5.6, wird diese Überprüfung automatisch mit strict aktiviert. Deshalb können TypeScript-Upgrades neue Fehler verursachen, auch wenn du deinen Code nicht änderst.
  • Du kannst strict aktivieren und dann einzelne Flags wieder deaktivieren. Explizite Flags schlagen immer den Master-Flag.

TypeScript 6.0 hat den Standardwert von strict auf true gesetzt. Trotzdem solltest du diese Flag in tsconfig.json explizit angeben, damit jemand, der die Config liest, nicht raten muss.

Um die Beispiele unten auszuprobieren, installiere einfach TypeScript und führe tsc --noEmit aus:

npm install --save-dev typescript
npx tsc --noEmit

3. Die wichtigsten Flags

3.1. noImplicitAny

Wenn TypeScript den Typ nicht ableiten kann, weist es stillschweigend any zu. Diese Flag macht diesen Fall zu einem Fehler.

function calculateTotal(items) {
  return items.reduce((sum, item) => sum + item.price, 0);
}
error TS7006: Parameter 'items' implicitly has an 'any' type.

Ohne diese Flag wäre items vom Typ any. Dann wäre item.prise (Tippfehler) trotzdem ein valider Code. Die Lösung ist, den Typ explizit zu deklarieren:

interface OrderItem {
  name: string;
  price: number;
}

function calculateTotal(items: OrderItem[]): number {
  return items.reduce((sum, item) => sum + item.price, 0);
}

Diese Flag blockiert nur implizites any. Du kannst immer noch any explizit schreiben, wenn du es wirklich brauchst.

3.2. strictNullChecks

Das ist der wertvollste Flag. Wenn deaktiviert, können null und undefined jedem Typ zugewiesen werden. Wenn aktiviert, werden sie zu eigenen Typen und du musst sie explizit behandeln.

interface Customer {
  id: number;
  email: string;
}

const customers: Customer[] = [{ id: 1, email: "an@example.com" }];

function getCustomerEmail(id: number): string {
  const customer = customers.find((c) => c.id === id);
  return customer.email;
}
error TS18048: 'customer' is possibly 'undefined'.

Array.prototype.find gibt Customer | undefined zurück. Wenn der Flag nicht aktiviert ist, ignoriert TypeScript undefined, und der Fehler Cannot read properties of undefined erscheint erst zur Laufzeit. Die Lösung:

function getCustomerEmail(id: number): string | undefined {
  const customer = customers.find((c) => c.id === id);
  return customer?.email;
}

Die Funktion gibt jetzt string | undefined zurück. Der Rückgabetyp sagt die Wahrheit, daher muss der Aufrufer auch den Fall behandeln, dass nichts gefunden wird.

Hinweis: “Behebe” den Fehler nicht mit dem Non-Null-Operator customer!.email. Der !-Operator befiehlt dem Compiler nur, zu schweigen; er überprüft nichts. Verwende ihn nur, wenn du sicher bist, dass der Wert existiert, und dein Grund für diese Sicherheit ist dem Compiler nicht ersichtlich.

3.3. strictPropertyInitialization

Diese Flag verlangt, dass jede Eigenschaft einer Klasse einen Wert erhält, entweder sofort bei der Deklaration oder im Konstruktor. Sie funktioniert nur mit strictNullChecks aktiviert.

class PaymentService {
  apiKey: string;
  retries = 3;

  constructor(private readonly baseUrl: string) {}
}
error TS2564: Property 'apiKey' has no initializer and is not definitely assigned in the constructor.

Du hast drei Möglichkeiten:

class PaymentService {
  apiKey: string;              // 1. im Konstruktor zuweisen
  webhookSecret?: string;      // 2. als optional deklarieren
  client!: HttpClient;         // 3. Definitive Assignment: "vertrau mir, wird zugewiesen"

  constructor(apiKey: string) {
    this.apiKey = apiKey;
  }
}

interface HttpClient {
  post(url: string, body: unknown): Promise<unknown>;
}

Option 3 (!) ist häufig bei Frameworks mit Dependency Injection oder Dekoratoren, wie Angular oder TypeORM. Dort wird die Eigenschaft nach dem Konstruktor zugewiesen. Außer in solchen Fällen solltest du Option 1 oder 2 bevorzugen.

3.4. useUnknownInCatchVariables

In JavaScript kannst du alles werfen, nicht unbedingt ein Error. Diese Flag ändert den Typ der Variable im catch von any zu unknown.

async function chargeOrder(orderId: string): Promise<void> {
  try {
    await fetch(`/api/orders/${orderId}/charge`, { method: "POST" });
  } catch (err) {
    console.error(err.message);
  }
}
error TS18046: 'err' is of type 'unknown'.

Um err zu verwenden, musst du zuerst den Typ eingrenzen:

catch (err) {
  const message = err instanceof Error ? err.message : String(err);
  console.error(`Charge failed: ${message}`);
}

3.5. strictFunctionTypes

Diese Flag überprüft Parameter von Funktionstypen contravariant. Einfach gesagt: Du kannst eine Funktion, die nur einen engen Typ verarbeitet, nicht an eine Stelle übergeben, wo eine Funktion mit breiterem Typ erwartet wird.

interface Animal { name: string; }
interface Dog extends Animal { bark(): void; }

const handleDog = (dog: Dog) => dog.bark();

const handleAnimal: (animal: Animal) => void = handleDog;
error TS2322: Type '(dog: Dog) => void' is not assignable to type '(animal: Animal) => void'.

Wenn diese Zuweisung erlaubt wäre, würde handleAnimal({ name: "Katze" }) crashen, weil das übergebene Objekt kein bark hat. Es gibt eine wichtige Ausnahme: Dieser Flag gilt nicht für Methoden, die als method(x: T): void in einem Interface deklariert sind. Das ist eine bewusste Entscheidung von TypeScript, um die Kompatibilität mit vielen bestehenden APIs zu bewahren.

3.6. Die verbleibenden drei Flags

Diese drei Flags verursachen selten Fehler in neu geschriebenem Code, sind aber trotzdem wichtig:

function formatPrice(amount: number, currency: string): string {
  return `${amount.toFixed(2)} ${currency}`;
}

formatPrice.call(null, "100", "VND"); // strictBindCallApply

function onClick() {
  console.log(this.id);               // noImplicitThis
}
error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.
error TS2683: 'this' implicitly has type 'any' because it does not have a type annotation.
  • strictBindCallApply überprüft Parameter, wenn du call, apply und bind aufrufst.
  • noImplicitThis gibt einen Fehler, wenn this implizit den Typ any hat. Du behebst das, indem du den Pseudo-Parameter function onClick(this: HTMLButtonElement) deklarierst.
  • alwaysStrict emittiert "use strict" in jede Output-Datei und parst Code im strict mode von JavaScript. Bei ES-Modulen ist strict mode bereits Standard.

4. Flags, die strict nicht aktiviert

Viele Menschen denken, strict: true ist die höchste Strictness-Stufe. Das stimmt nicht. Diese Flags liegen außerhalb der strict-Gruppe, sind aber sehr empfehlenswert für neue Projekte:

{
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
    "noImplicitOverride": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true
  }
}

Von diesen ist noUncheckedIndexedAccess der wichtigste Flag. Standardmäßig geht TypeScript davon aus, dass Array-Zugriff über Index oder dynamischer Objektzugriff immer einen Wert liefert:

const stockBySku: Record<string, number> = { "SKU-1": 10 };

const quantity = stockBySku["SKU-999"];
console.log(quantity.toFixed(0));

Ohne diesen Flag hat quantity den Typ number und dieser Code crasht zur Laufzeit. Mit dem Flag aktiviert hat quantity den Typ number | undefined:

error TS18048: 'quantity' is possibly 'undefined'.

Die verbleibenden Flags:

  • exactOptionalPropertyTypes: discount?: number bedeutet “key kann fehlen”, erlaubt aber keine explizite Zuweisung von discount: undefined.
  • noImplicitOverride: Eine Methode, die eine Methode der Elternklasse überschreibt, muss das Schlüsselwort override haben. Dadurch wird ein Fehler gemeldet, wenn die Elternklasse eine Methode umbenennt, statt dass die Kindklasse stumm eine neue Methode erstellt.
  • noImplicitReturns: Jeder Branch einer Funktion muss return haben.
  • noFallthroughCasesInSwitch: Verhindert, dass man in einem switch das break vergisst.

Hinweis: noUncheckedIndexedAccess ist etwas “laut”. Eine Schleife wie for (let i = 0; i < arr.length; i++) ergibt auch arr[i] mit Typ T | undefined. Wechsel zu for...of oder arr.forEach. Bei großen bestehenden Projekten solltest du diesen Flag zuletzt aktivieren.

5. Verhalten mit Tests überprüfen

Type-Überprüfung läuft zur Compile-Zeit, aber du solltest immer noch Tests für sowohl Laufzeit-Verhalten als auch Rückgabetypen haben. Vitest unterstützt beides:

npm install --save-dev vitest
// customer.ts
export interface Customer { id: number; email: string; }

const customers: Customer[] = [{ id: 1, email: "an@example.com" }];

export function getCustomerEmail(id: number): string | undefined {
  return customers.find((c) => c.id === id)?.email;
}
// customer.test.ts
import { describe, it, expect, expectTypeOf } from "vitest";
import { getCustomerEmail } from "./customer";

describe("getCustomerEmail", () => {
  it("returns email when customer is found", () => {
    expect(getCustomerEmail(1)).toBe("an@example.com");
  });

  it("returns undefined when not found", () => {
    expect(getCustomerEmail(42)).toBeUndefined();
  });

  it("return type includes undefined", () => {
    expectTypeOf(getCustomerEmail).returns.toEqualTypeOf<string | undefined>();
  });
});

Der dritte Test ist ein Type-Test. Angenommen, später ändert jemand die Funktionssignatur zu : string und verwendet ! um den Typ zu erzwingen. Dann gibt vitest --typecheck (oder tsc --noEmit auf der Test-Datei) einen Fehler, weil der Rückgabetyp nicht mehr mit string | undefined übereinstimmt.

npx vitest run --typecheck
 ✓ customer.test.ts (3 tests)
 Test Files  1 passed (1)
      Tests  3 passed (3)

6. Ein älteres Projekt zu strict migrieren

Das Aktivieren von strict: true auf einer großen Codebasis kann Tausende von Fehlern verursachen. Der praktische Weg ist, Schritt für Schritt vorzugehen.

Schritt 1: Aktiviere strict, aber deaktiviere vorübergehend die lautesten Flags.

{
  "compilerOptions": {
    "strict": true,
    "strictNullChecks": false,
    "strictPropertyInitialization": false,
    "useUnknownInCatchVariables": false
  }
}

noImplicitAny ist normalerweise am leichtesten zu beheben, also kümmere dich zuerst darum. Jedes Mal wenn du einen Flag behoben hast, kannst du die Deaktivierungszeile entfernen. Neuer Code wird automatisch alle aktivierten Überprüfungen unterliegen.

Schritt 2: Zähle Fehler pro Flag, um den Aufwand zu schätzen.

npx tsc --noEmit --strictNullChecks true | grep -c "error TS"

Schritt 3: Mit strictNullChecks vorankommen, Verzeichnis für Verzeichnis. Markiere vorübergehend unvollendete Stellen mit // @ts-expect-error, zusammen mit einer Notiz warum. Verwende nicht @ts-ignore. Grund: @ts-expect-error wird selbst einen Fehler melden, wenn die Zeile darunter nicht mehr fehlerhaft ist, so dass du keine verwaisten Kommentare übersehen wirst.

// @ts-expect-error TODO(strict): customer könnte undefined sein, bearbeitet in PR #482
const email: string = findCustomer(id).email;

Schritt 4: Regress in CI blockieren. Füge tsc --noEmit zu deiner Pipeline hinzu, damit niemand Code mergen kann, der neue Type-Fehler einführt.

Hinweis: Verwende nicht skipLibCheck oder verbreite any massiv als “Schnell-Lösung” um strict zu aktivieren. skipLibCheck: true überspringt nur die Überprüfung von .d.ts Dateien, das ist okay zu behalten. Aber any überall zu streuen wird dir alle Vorteile der Migration rauben.

7. Fazit

strict: true aktiviert neun Überprüfungen. Die zwei wertvollsten sind noImplicitAny und strictNullChecks, da sie die meisten Fehler wie undefined is not a function und Cannot read properties of undefined bereits zur Compile-Zeit verhindern. Bei neuen Projekten solltest du strict zusammen mit noUncheckedIndexedAccess, noImplicitOverride und exactOptionalPropertyTypes von Anfang an aktivieren. Bei bestehenden Projekten aktiviere strict, deaktiviere dann vorübergehend einzelne Flags, behebe sie Schritt für Schritt, von einfach zu schwierig, und nutze CI um sicherzustellen, dass die Fehleranzahl nur sinkt statt zu steigen.

typescripttsconfigstrict modetype safety