Prüfungen nachschärfen

strict im Bestand: was es kostet und was es bringt

Neun Einzelschalter, ein Ordner nach dem anderen und eine Prüfung, die im Build blockiert. So wird aus einer Zahl im Terminal ein Plan, den ein Team durchhält.

KI-generiertDieses Bild wurde mit KI erzeugt · Yves Hoppe / KI / cmt
Seit 1997 am Markt Kleine Gruppen Präsenz und Live-Online Zertifizierte Trainer
Worum es geht

Die Zahl im Terminal sagt weniger, als alle glauben

Der übliche Ablauf: Jemand setzt strict in der tsconfig auf true, lässt tsc laufen, sieht 1.400 Fehler und schaltet es wieder aus. Danach steht im Team die Aussage im Raum, das sei nicht machbar. Die Zahl trägt diese Aussage aber nicht, weil sie kein Maß für Aufwand ist. Ein einziger falsch deklarierter Rückgabewert in einem oft genutzten Modul erzeugt Dutzende Folgemeldungen an ganz anderen Stellen, und die verschwinden alle mit einer Zeile.

Umgekehrt steckt echter Aufwand dort, wo strictNullChecks eine Annahme sichtbar macht, die im Code nie geprüft wurde. Wenn eine Funktion seit Jahren davon ausgeht, dass ein Feld gefüllt ist, ist die Meldung kein Formalismus, sondern die Frage, was passieren soll, wenn es leer ist. Diese Frage kostet Zeit, und zwar nicht beim Tippen, sondern beim Nachdenken über die Fachlichkeit. Genau das ist auch der Gewinn.

Wer sich dagegen entscheidet, zahlt an anderer Stelle. Ohne noImplicitAny wandert jeder untypisierte Parameter als stiller Freifahrtschein durch den Code und nimmt allen nachgelagerten Prüfungen die Grundlage. Ohne strictNullChecks ist die häufigste Ausnahme in der Produktion weiterhin der Zugriff auf etwas, das undefined war. Die Entscheidung lautet also nicht sauber gegen pragmatisch, sondern: jetzt in Etappen bezahlen oder später einzeln in Fehlerberichten.

KI-generiertDieses Bild wurde mit KI erzeugt · Yves Hoppe / KI / cmt
Wer entscheidet was

Wer entscheidet was

Ob strict kommt und bis wann

Wer entscheidet
Die Teamleitung zusammen mit den Leuten, die den Code täglich anfassen, und zwar auf Basis einer Messung statt einer Schätzung.
Wer setzt um
Wer die Messung macht: einmal einschalten, tsc mit noEmit laufen lassen, Meldungen nach Ordner und Fehlercode zählen, wieder ausschalten.
Stolperfalle
Die Gesamtzahl der Meldungen wird als Aufwand gelesen. Ein einziger falscher Rückgabetyp in einem zentralen Modul erzeugt aber Dutzende Folgemeldungen, die gemeinsam verschwinden.

Reihenfolge der Einzelschalter

Wer entscheidet
Wer die Architektur verantwortet, weil die Reihenfolge davon abhängt, wo die Daten ins System kommen.
Wer setzt um
Das jeweilige Team im eigenen Bereich, beginnend bei noImplicitAny und noImplicitThis.
Stolperfalle
strictNullChecks wird mit eingeschaltet, weil es zur selben Option gehört. Es braucht aber eigene Zeit und einen eigenen Termin, denn es lässt sich nicht halb einführen.

Umgang mit den Stellen, die noch nicht gehen

Wer entscheidet
Das Team gemeinsam, als Konvention im Repository festgehalten.
Wer setzt um
Wer die Datei umstellt: @ts-expect-error mit einem Satz Begründung und einer Vorgangsnummer, nie @ts-ignore.
Stolperfalle
Das Ausrufezeichen als Abkürzung. Es unterdrückt die Meldung, prüft nichts und ist im Suchlauf später kaum von berechtigten Fällen zu unterscheiden.

Ab wann die Prüfung blockiert

Wer entscheidet
Wer den Build verantwortet, im Einvernehmen mit den Teams, deren Ordner betroffen sind.
Wer setzt um
Ein zusätzlicher Schritt in der Pipeline, der tsconfig.strict.json prüft und bei einem Fehler abbricht.
Stolperfalle
Als bloße Warnung gestartet und nie scharf geschaltet. Nach zwei Wochen liest die Ausgabe niemand mehr, und die Zahl steigt wieder.

Fremde Typen und generierte Dateien

Wer entscheidet
Wer die Abhängigkeiten pflegt.
Wer setzt um
skipLibCheck einschalten, generierte Ordner in exclude aufnehmen, fehlende Typpakete nachziehen.
Stolperfalle
skipLibCheck nimmt auch die eigenen Deklarationsdateien aus der Prüfung. Wer selbst .d.ts pflegt, verliert genau dort die Kontrolle.

Neun Prüfungen stecken hinter der einen Option

  1. 01 noImplicitAny meldet jeden Parameter, dessen Typ niemand angegeben hat.
  2. 02 noImplicitThis meldet ein this, dessen Typ der Compiler nicht kennt.
  3. 03 alwaysStrict stellt jede Datei in den strikten Modus der Sprache.
  4. 04 strictNullChecks trennt null und undefined von allen anderen Typen.
  5. 05 strictFunctionTypes prüft Parameter von Funktionstypen strenger.
  6. 06 strictPropertyInitialization verlangt einen Wert für jedes Klassenfeld.
  7. 07 strictBindCallApply prüft die Argumente von bind, call und apply.
  8. 08 useUnknownInCatchVariables macht den Fehler im catch-Block zu unknown.
  9. 09 strictBuiltinIteratorReturn kam mit TypeScript 5.6 für eingebaute Iteratoren dazu.
Was du mitnimmst

Was du danach entscheiden kannst, ohne zu raten

Die Einführung ist weniger eine technische als eine organisatorische Aufgabe. Der Compiler kann alles, was du brauchst, aber jemand muss festlegen, in welcher Reihenfolge verschärft wird, was mit den Ausnahmen passiert und ab wann die Prüfung wirklich blockiert.

Den Aufwand messen statt schätzen

Du schaltest strict einmal ein, lässt tsc mit noEmit laufen, gruppierst die Meldungen nach Ordner und Fehlercode und schaltest wieder aus. Erst diese Verteilung sagt dir, wo die Arbeit steckt, und nicht die Gesamtzahl.

Die Einzelschalter in der richtigen Reihenfolge ziehen

noImplicitAny und noImplicitThis kommen zuerst, weil sie die Grundlage für alles Weitere legen. strictNullChecks kommt zuletzt und bekommt eigene Zeit, weil es sich nicht sinnvoll halb einführen lässt.

Eine zweite Konfiguration statt eines Stichtags

Du legst eine tsconfig.strict.json an, die die Basis erweitert, strict einschaltet und nur die bereits bereinigten Ordner einschließt. Der eigentliche Build bleibt unverändert, die strenge Prüfung läuft zusätzlich.

Ausnahmen wählen, die von selbst verschwinden

Du nimmst @ts-expect-error statt @ts-ignore. Sobald die Stelle repariert ist, meldet der Compiler die überflüssige Ausnahme, und die Zeile fliegt raus. @ts-ignore bleibt dagegen für immer stehen.

Den Zeitpunkt für das Blockieren festlegen

Solange die strenge Prüfung nur eine Warnung ist, liest sie nach zwei Wochen niemand mehr. Du legst deshalb fest, ab wann ein Pull Request daran scheitert, und für welche Ordner das schon gilt.

Fremde und generierte Dateien aus dem Weg räumen

Mit skipLibCheck nimmst du die Deklarationsdateien aus der Prüfung, und generierte Ordner gehören in exclude. Dir ist dabei klar, dass skipLibCheck auch deine eigenen .d.ts-Dateien nicht mehr prüft.

KI-generiertDieses Bild wurde mit KI erzeugt · Yves Hoppe / KI / cmt

Was strict überhaupt einschaltet

strict ist kein eigener Modus, sondern ein Sammelschalter für neun Einzelprüfungen: alwaysStrict, noImplicitAny, noImplicitThis, strictBindCallApply, strictBuiltinIteratorReturn, strictFunctionTypes, strictNullChecks, strictPropertyInitialization und useUnknownInCatchVariables. strictBuiltinIteratorReturn ist mit TypeScript 5.6 dazugekommen, und das ist kein Einzelfall: Unter strict können weitere Prüfungen nachrücken, ein Sprung auf eine neue Compiler-Version kann also neue Meldungen bringen. Wichtig für den Bestand ist, dass sich jede dieser Prüfungen einzeln übersteuern lässt. Du kannst also strict auf true setzen und darunter strictNullChecks vorübergehend auf false, statt alles gemeinsam abzuwarten. Damit sicherst du die einfacheren Prüfungen sofort und nimmst dir die aufwendigste getrennt vor.

Nicht in strict enthalten sind einige Optionen, die im Gespräch oft dazugezählt werden: noUncheckedIndexedAccess, exactOptionalPropertyTypes, noImplicitOverride, noPropertyAccessFromIndexSignature sowie die Aufräumhilfen noUnusedLocals und noUnusedParameters. Vor allem noUncheckedIndexedAccess ist eine eigene Entscheidung mit spürbaren Folgen, weil danach jeder Zugriff über einen Index oder einen dynamischen Schlüssel auch undefined liefern kann. Das ist fachlich richtig und erzeugt in Code, der viel mit Listen und Nachschlagetabellen arbeitet, noch einmal eine eigene Welle.

Warum strictNullChecks alleine steht

Ohne diese Prüfung sind null und undefined Mitglieder jedes Typs. Ein string kann also ohne Beanstandung null sein, und jede Zusicherung im Code ist damit eine Behauptung ohne Deckung. Mit der Prüfung werden beide zu eigenen Typen, die ausdrücklich in einer Vereinigung stehen müssen. Genau deshalb erzeugt sie den Großteil aller Meldungen und lässt sich nicht in kleinen Häppchen einführen: Sobald sie an ist, betrifft sie jede Zeile, die eine Variable liest.

Der Aufwand verteilt sich dabei sehr ungleich. In reinen Rechenfunktionen passiert fast nichts. Teuer wird es an den Rändern: bei Antworten aus dem Netz, bei Zugriffen aufs DOM, bei Konfigurationswerten und überall dort, wo eine Funktion optionale Parameter hat. Deshalb lohnt es sich, vor dem Einschalten genau diese Ränder zu bereinigen, also einmal ordentlich zu typisieren, was aus dem Netz kommt. Danach fällt ein erheblicher Teil der Meldungen weg, ohne dass jemand eine einzige if-Abfrage geschrieben hätte.

Beim Reparieren gibt es einen Umweg, der teuer ist und deshalb eine Absprache verdient: das Ausrufezeichen hinter einem Ausdruck, also wert!.name. Es sagt dem Compiler, dass hier sicher kein null steht, prüft aber nichts. In wenigen Fällen ist das richtig, etwa wenn ein Element garantiert im HTML des eigenen Templates steht. Als allgemeine Antwort auf Meldungen macht es die Einführung von strict wertlos, weil das Ergebnis genau die Sicherheit vortäuscht, die vorher gefehlt hat.

Der Weg über eine zweite Konfiguration

Statt eines Stichtags legst du eine tsconfig.strict.json an, die per extends auf der bestehenden Konfiguration aufsetzt, dort strict einschaltet und in include nur die Ordner aufzählt, die schon bereinigt sind. Der normale Build läuft unverändert weiter, die strenge Prüfung startet zusätzlich mit tsc -p tsconfig.strict.json --noEmit. Wächst die Liste in include, wächst der geschützte Bereich, und niemand muss auf einen großen Umbau warten.

Diese Konstruktion hat einen Haken, den man kennen sollte: Der Compiler prüft auch Dateien, die von den eingeschlossenen importiert werden, selbst wenn sie nicht in include stehen. In der Praxis bedeutet das, dass du an den Blättern des Abhängigkeitsbaums anfängst, also bei Hilfsfunktionen und Datenmodellen ohne eigene Importe. Von dort arbeitest du dich nach oben. Wer bei einer Seitenkomponente beginnt, zieht in der ersten Runde das halbe Projekt mit hinein.

Für die Steuerung im Alltag reicht ein einziges Signal: die Zahl der Meldungen aus dem strengen Lauf, festgehalten je Woche. Sie darf nur fallen. Sobald sie steigt, ist entweder ein Ordner zu früh in include gewandert oder jemand hat in einem geschützten Bereich neue Schulden angelegt. Diese eine Zahl ersetzt jede Diskussion darüber, ob es vorangeht.

Wann sich der Aufwand für ein Team nicht lohnt

Wenn eine Anwendung nur noch Fehlerbehebungen bekommt und in absehbarer Zeit abgelöst wird, ist die Verschärfung ein Umbau ohne Empfänger. Dasselbe gilt für Code, der ohnehin generiert wird, etwa Klienten aus einer Schnittstellenbeschreibung: Dort gehört die Strenge in den Generator und nicht in die erzeugten Dateien.

Umgekehrt gibt es drei Lagen, in denen sich der Aufwand fast immer rechnet. Erstens, wenn regelmäßig neue Leute dazukommen, denn strenge Typen sind die einzige Dokumentation, die nicht veraltet. Zweitens, wenn dieselbe Datenstruktur durch viele Module fließt, weil dort ein einzelnes any die Prüfung auf dem ganzen Weg abschaltet. Und drittens, wenn du ohnehin eine größere Umbaumaßnahme vor dir hast: Ein Refactoring unter strict ist eine andere Tätigkeit als eines ohne, weil der Compiler dir die betroffenen Stellen zeigt, statt sie dir zu überlassen.

Dazu passende Kurse

Wer im laufenden Projekt strengere Prüfungen nachträglich einführen will, findet den passenden Stoff in den TypeScript-Kursen.

Weil eine strenge Prüfung nur wirkt, wenn sie in der Pipeline blockiert, passen dazu Kurse rund um automatisierte Prüfungen im Build .

Wissen prüfen

Wie sicher bist du beim Thema wirklich?

Lesen fühlt sich schnell nach Können an. Ein kurzer Test zeigt dir, was davon schon sitzt und wo sich ein Kurs lohnt. Kostenlos, ohne Anmeldung, mit einer Erklärung zu jeder Antwort.

Ein sehr gutes, praxisorientiertes und nachhaltiges Seminar. So sollte es immer sein. Vielen Dank.
LimeSurvey - Anwendertraining Teil 1 (Grundlagen)
Die Schulung ist gerade für Einsteiger oder Entwickler*innen mit eingestaubtem Basiswissen sehr hilfreich.
Vue.js 3 Grundkurs
Ich bin sehr glücklich Franz als Trainer gehabt zu haben. Er hat offensichtlich unglaublich viel Wissen zu den Themen, und schafft es, dieses auch interaktiv und verständlich weiterzugeben.
React Komplettausbildung

Häufige Fragen

Kann ich strict einschalten und einzelne Prüfungen wieder abschalten?
Ja, und das ist im Bestand der übliche Weg. Die Einzeloptionen haben Vorrang vor dem Sammelschalter, du kannst also strict auf true setzen und strictNullChecks darunter auf false. Damit greifen sofort acht von neun Prüfungen, und die aufwendigste bekommt einen eigenen Termin. Wichtig ist nur, dass dieser Termin existiert und irgendwo notiert ist.
Wie lange dauert das in einem mittelgroßen Projekt?
Eine ehrliche Antwort gibt es nur nach der Messung, weil der Aufwand nicht an der Zeilenzahl hängt, sondern daran, wie viele Systemgrenzen es gibt und wie sauber sie bisher typisiert waren. Verlässlich ist die Beobachtung, dass die ersten zwanzig Prozent der Meldungen den größten Teil der Zeit brauchen: Dort steckt meist ein zentrales Datenmodell, das erst einmal richtig beschrieben werden muss.
Ist @ts-expect-error nicht genauso schlimm wie @ts-ignore?
Nein, und der Unterschied ist praktisch. @ts-ignore unterdrückt die Meldung dauerhaft, auch wenn die Stelle längst repariert ist. @ts-expect-error verlangt, dass an dieser Stelle tatsächlich ein Fehler auftritt. Sobald die Ursache behoben ist, meldet der Compiler die Ausnahme als überflüssig. Damit räumt der Code sich in dieser Hinsicht selbst auf.
Bringt strict etwas, wenn wir viel mit Daten aus dem Netz arbeiten?
Ja, aber nicht von allein. Der Compiler prüft, was du behauptest, und keine Antwort aus dem Netz prüft sich selbst. Wenn du eine Antwort einfach als deinen Typ zusicherst, ist die Strenge an dieser Stelle wirkungslos. Der Gewinn entsteht, sobald du an der Systemgrenze einmal ausdrücklich prüfst, etwa mit einem Schema, und der Typ danach aus dieser Prüfung stammt.
Persönlich für dich da

Deine Ansprechpartner

Du bist dir nicht sicher, welcher Kurs oder welches Level zu dir passt? Wir beraten dich persönlich und kostenlos.

Yves Hoppe

Yves Hoppe

Weiterbildung & Beratung

Hilft dir, aus dem Webentwicklung-Programm den passenden Kurs für deinen Stand zu finden.

Norbert Jansen

Norbert Jansen

Beratung & Inhouse

Plant mit dir Inhouse-Trainings, die auf eure Abläufe und euren Datenbestand zugeschnitten sind.

Strenge Typen einführen, ohne das Projekt anzuhalten

In den TypeScript-Kursen bei cmt arbeitest du an Code mit Altlasten und siehst, an welchen Stellen die Verschärfung wirklich Arbeit macht und an welchen sie sich von selbst erledigt.