Löcher im Typsystem

Warum ein einziges any eine ganze Kette entwertet

Der Wert kommt aus dem Netz, wird weitergereicht und landet in einer typisierten Funktion, ohne dass der Compiler auch nur einmal hinsieht. unknown unterbricht genau diese Kette.

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

Der Schaden entsteht nicht an der Stelle mit dem any, sondern drei Module weiter

any ist der einzige Typ, der in beide Richtungen zuweisbar ist. Ein string lässt sich einem any zuweisen, und ein any lässt sich einem string zuweisen, einem eigenen Datentyp, einem Funktionstyp, einer Liste. Der Compiler beanstandet nichts davon. Sobald ein Wert also einmal als any im Code ist, reist er ungeprüft durch alle Funktionen, die er berührt, und die Typen dieser Funktionen sind für ihn nur noch Dekoration.

Praktisch sieht das so aus: await response.json() ist mit Promise<any> deklariert, das Ergebnis geht in eine Aufbereitung, von dort in eine Komponente, die eigentlich einen sauber beschriebenen Datentyp erwartet. Der Editor schlägt unterwegs nichts vor, weil er nichts weiß, und der Compiler schweigt, weil er nichts prüfen darf. Auffällig wird das erst, wenn im Backend ein Feld umbenannt wird: Der Build läuft durch, die Tests laufen durch, und in der Anwendung steht undefined.

Der zweite, teurere Effekt zeigt sich beim Umbenennen. Ein Refactoring über die Entwicklungsumgebung findet nur Stellen, die typisiert sind. Alles, was über eine any-Kette läuft, bleibt stehen, und zwar still. Genau deshalb ist any nicht eine Bequemlichkeit für heute mit Aufräumarbeit für morgen, sondern eine Wette darauf, dass sich die Datenstruktur nie wieder ändert.

KI-generiertDieses Bild wurde mit KI erzeugt · Yves Hoppe / KI / cmt
Symptom, Ursache, Lösung

Symptom, Ursache, Lösung

Symptom

Der Editor schlägt nach dem Punkt gar nichts vor, obwohl das Objekt eindeutig Felder hat, und ein Tippfehler im Feldnamen bleibt ohne Meldung.

Ursache

Der Wert stammt aus await response.json() oder JSON.parse. Beide sind mit any deklariert, weil zur Übersetzungszeit niemand weiß, was im Text steht. Ab dieser Zeile ist der gesamte weitere Pfad ungeprüft.

Lösung

Nimm das Ergebnis ausdrücklich als unknown entgegen und schieb es durch einen Typwächter oder ein Schema. Der Wert bekommt dabei genau einmal einen Typ, und zwar an der Systemgrenze statt irgendwo in der Mitte.

Symptom

Ein Feld wird im Backend umbenannt, der Build läuft durch, die Tests laufen durch, und in der Oberfläche steht undefined.

Ursache

Der Wert reist als any durch mehrere Module. Weil any in beide Richtungen zuweisbar ist, nimmt ihn auch eine sauber typisierte Funktion ohne Beanstandung entgegen, und deren Typangabe wird zur reinen Behauptung.

Lösung

Unterbrich die Kette an ihrer Quelle statt am Ende. Sobald der Wert an der Eingangsstelle einen echten Typ bekommt, meldet der Compiler das umbenannte Feld an allen Stellen, an denen es benutzt wird.

Symptom

Nach dem Einschalten von strict ist in jedem catch-Block der Zugriff auf fehler.message rot markiert.

Ursache

useUnknownInCatchVariables gehört zu strict und macht den gefangenen Wert zu unknown. Das ist fachlich richtig, denn in JavaScript lässt sich jeder Wert werfen, auch eine Zeichenkette oder undefined.

Lösung

Prüf mit fehler instanceof Error, bevor du auf message zugreifst, und leg einen Rückfallweg fest, etwa String(fehler). Wenn das an vielen Stellen steht, gehört es in eine kleine Hilfsfunktion, die aus unknown eine verlässliche Meldung macht.

Symptom

Ein Import aus einer Bibliothek ist nicht mehr rot, aber der Editor kennt an keinem Aufruf die Parameter, und falsche Argumente fallen nicht auf.

Ursache

Irgendwo im Projekt steht eine Kurzdeklaration der Form declare module 'paketname'; . Sie beruhigt den Compiler und macht den gesamten Inhalt des Pakets zu any, inklusive aller Rückgabewerte.

Lösung

Prüf zuerst, ob es ein Typpaket unter @types gibt oder die Bibliothek inzwischen eigene Typen mitbringt. Wenn nicht, ersetz die Kurzdeklaration durch eine eigene Deklarationsdatei mit genau den Funktionen, die ihr tatsächlich aufruft.

Symptom

Der Linter meldet no-unsafe-assignment in einer Datei, in der das Wort any überhaupt nicht vorkommt.

Ursache

Der ungeprüfte Wert stammt aus einem Import. Die Regeln der no-unsafe-Familie melden nicht die Quelle, sondern jede Verwendung, und das ist genau ihr Zweck.

Lösung

Folge dem Import bis zur Quelle und repariere dort. Eine einzelne bereinigte Eingangsstelle bringt in dieser Regelfamilie oft ein bis zwei Dutzend Meldungen gleichzeitig zum Verschwinden.

Sechs Wege, auf denen any ins Projekt kommt

  1. 01 JSON.parse liefert any, unabhängig davon, was tatsächlich im Text stand.
  2. 02 await response.json() ist als Promise<any> deklariert und öffnet denselben Weg.
  3. 03 Eine Kurzdeklaration für ein Paket macht dessen gesamten Inhalt zu any.
  4. 04 Ohne noImplicitAny bekommt jeder Parameter ohne Typangabe still ein any.
  5. 05 Ein as any im Test wandert beim Kopieren in den Produktivcode.
  6. 06 Der Fehler im catch-Block war vor useUnknownInCatchVariables ebenfalls any.
Was du mitnimmst

Was du danach an der Systemgrenze richtig machst

Es geht nicht darum, jedes any im Projekt zu tilgen. Es geht darum, dass ein Wert genau einmal einen Typ bekommt, nämlich an der Stelle, an der er ins System kommt, und dass diese Stelle bewusst gebaut ist statt nebenbei entstanden.

unknown als Eingangstür benutzen

Du nimmst fremde Werte als unknown entgegen. Damit ist jede Verwendung gesperrt, bis du geprüft hast, was vorliegt. Genau diese Sperre fehlt bei any, und sie ist der ganze Unterschied zwischen den beiden.

Typwächter schreiben, die etwas prüfen

Eine Funktion mit dem Rückgabetyp wert is Kunde verengt den Typ für den Compiler. Du weißt dabei, dass der Compiler dir glaubt, ohne nachzusehen, und schreibst deshalb im Rumpf tatsächlich die Prüfungen hin, die der Rückgabetyp verspricht.

Schemas statt Handarbeit an großen Strukturen

Für Antworten mit vielen Feldern beschreibst du die Struktur einmal als Schema und leitest den TypeScript-Typ daraus ab. Damit können Prüfung und Typ nicht mehr auseinanderlaufen, weil beide aus derselben Quelle stammen.

Den Fehler im catch-Block richtig behandeln

Seit useUnknownInCatchVariables ist der gefangene Wert unknown und nicht any, denn geworfen werden kann alles. Du prüfst mit fehler instanceof Error, bevor du auf message zugreifst, und hast damit auch die Fälle abgedeckt, in denen eine Zeichenkette geworfen wurde.

Bibliotheken ohne Typen sauber anbinden

Statt einer Kurzdeklaration, die das ganze Paket zu any macht, schreibst du eine eigene Deklarationsdatei mit genau den Funktionen, die ihr tatsächlich benutzt. Das sind meist drei bis fünf, und der Rest interessiert niemanden.

Den Bestand sichtbar machen

Du weißt, dass noImplicitAny nur die stillschweigenden Fälle findet. Die ausdrücklich hingeschriebenen holst du über eine Linter-Regel, und den Anteil ungeprüfter Stellen misst du über eine Abdeckungszahl, die im Build ausgegeben wird.

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

unknown ist any mit einer Schranke davor

Beide Typen nehmen jeden Wert entgegen, das ist die Gemeinsamkeit. Der Unterschied liegt beim Herausgehen: Aus einem any darfst du alles machen, aus einem unknown zunächst nichts. Kein Zugriff auf Eigenschaften, kein Aufruf, keine Zuweisung an einen engeren Typ. Der Compiler zwingt dich damit an genau eine Stelle, an der du festlegst, was der Wert ist. Diese eine Stelle ist der ganze Gewinn, denn sie ist auffindbar, während ein any über zehn Dateien verteilt liegt.

Verengen kannst du auf mehreren Wegen. Für einfache Fälle reichen typeof und instanceof, weil TypeScript beide versteht und den Typ im nachfolgenden Block automatisch einschränkt. Für Objekte brauchst du eine ausdrückliche Prüfung, die auch das Ergebnis von typeof null berücksichtigt: typeof wert === 'object' && wert !== null. Danach kommst du mit dem in-Operator an die Felder, also 'kundennummer' in wert. Das ist umständlicher als ein Punkt, aber es ist genau die Arbeit, die vorher niemand gemacht hat.

Typwächter und Zusicherungsfunktionen

Ein Typwächter ist eine gewöhnliche Funktion mit einem besonderen Rückgabetyp: function istKunde(wert: unknown): wert is Kunde. Wo sie true liefert, behandelt der Compiler den Wert im folgenden Block als Kunde. Der entscheidende Punkt: Der Compiler prüft nicht, ob dein Rumpf diese Behauptung wirklich trägt. Eine Funktion, die nur typeof wert === 'object' zurückgibt, aber Kunde verspricht, ist ein as any mit besserer Tarnung. Schreib deshalb die Prüfungen aus, und zwar für die Felder, auf die der Code danach tatsächlich zugreift.

Für den Fall, dass es kein sinnvolles Weiter gibt, nimmst du eine Zusicherungsfunktion: function pruefeKunde(wert: unknown): asserts wert is Kunde. Sie wirft, wenn etwas nicht stimmt, und danach gilt der Typ für den Rest des Blocks, ohne dass du eine if-Verzweigung brauchst. Eine Eigenheit kostet regelmäßig eine Viertelstunde: Zusicherungsfunktionen brauchen am Aufrufziel eine ausdrückliche Typangabe. Eine als const gespeicherte Pfeilfunktion ohne eigene Typannotation lehnt der Compiler mit der Begründung ab, dass der Name keine ausdrücklich deklarierte Signatur hat. Als normale Funktionsdeklaration mit dem Schlüsselwort function funktioniert derselbe Code sofort.

Schemas an der Systemgrenze

Bei einer Antwort mit zwanzig Feldern wird der handgeschriebene Typwächter selbst zur Fehlerquelle, weil er neben dem Typ gepflegt werden muss und dabei auseinanderläuft. Der übliche Ausweg sind Bibliotheken zur Schemaprüfung wie zod oder valibot: Du beschreibst die Struktur einmal, bekommst daraus eine Prüffunktion und leitest den TypeScript-Typ aus demselben Schema ab. Ändert sich das Schema, ändert sich der Typ mit, und Prüfung und Deklaration können gar nicht mehr auseinanderfallen.

Die Kosten dieser Lösung sind real und überschaubar: eine Abhängigkeit mehr und eine Prüfung zur Laufzeit, die Rechenzeit braucht. Deshalb gehört sie an die Ränder und nicht in jede interne Funktion. Sinnvolle Ränder sind Antworten fremder Dienste, Formulardaten, Werte aus dem lokalen Speicher des Browsers, Konfigurationsdateien und Umgebungsvariablen. Innerhalb des eigenen Systems reichen die statischen Typen, denn dort hast du beide Seiten unter Kontrolle.

Ein Sonderfall verdient eine Absprache im Team: Antworten aus einer eigenen, ebenfalls typisierten Schnittstelle. Dort ist die Versuchung groß, die Antwort einfach zuzusichern, weil man den Vertrag ja kennt. Das trägt genau so lange, wie beide Seiten gemeinsam ausgeliefert werden. Sobald Frontend und Backend getrennt veröffentlicht werden, ist die Zusicherung eine Annahme über eine Version, die gerade läuft, und keine Aussage über die, die morgen läuft.

Bestehende Vorkommen finden und abbauen

noImplicitAny findet nur die stillschweigenden Fälle, also Parameter und Variablen ohne Typangabe. Die ausdrücklich hingeschriebenen any bleiben davon unberührt, dafür brauchst du eine Linter-Regel. In typescript-eslint heißt sie no-explicit-any. Deutlich aufschlussreicher ist allerdings die Familie no-unsafe-assignment, no-unsafe-member-access, no-unsafe-call, no-unsafe-return und no-unsafe-argument: Diese Regeln melden nicht das any selbst, sondern jede Stelle, an der ein ungeprüfter Wert weiterverwendet wird. Damit siehst du erstmals, wie weit eine einzelne Quelle ausstrahlt. Voraussetzung ist eine typbewusste Linter-Konfiguration, die das Projekt beim Prüfen mitlädt, und die läuft spürbar langsamer.

Für eine Zahl, die sich verfolgen lässt, gibt es das Werkzeug type-coverage. Es meldet den Anteil der Ausdrücke im Projekt, die einen echten Typ haben. Der absolute Wert sagt wenig, die Richtung sagt alles: Du legst den aktuellen Stand als Untergrenze im Build fest und ziehst sie nach, wenn sie steigt. Damit kann der Anteil nicht mehr zurückfallen, ohne dass es auffällt.

Beim Abbauen lohnt eine Reihenfolge. Zuerst die Quellen, also die Stellen, an denen any entsteht, denn eine einzige davon trocknet oft zwanzig Folgestellen aus. Danach die Zusicherungen mit as any, die fast immer eine Notlösung waren und häufig durch einen kleinen Zwischentyp ersetzt werden können. Ganz zum Schluss die Fälle, in denen any tatsächlich richtig ist, etwa in einer sehr allgemeinen Protokollierungsfunktion. Die dürfen bleiben, sollten aber einen Kommentar tragen, damit sie beim nächsten Durchgang nicht erneut jemanden beschäftigen.

Dazu passende Kurse

Wenn du im eigenen Code nachvollziehen willst, wo der Allerwelts-Typ ins Projekt sickert , ist das ein fester Bestandteil der TypeScript-Kurse.

Weil jede Prüfung am Ende in gewöhnlichem JavaScript landet, lohnt sich daneben ein Blick auf Kurse zu den Sprachmitteln unter TypeScript .

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

Ist any nie richtig?
Doch, es gibt Fälle. Eine Protokollierungsfunktion, die tatsächlich alles entgegennehmen soll, ist ein Beispiel, ebenso der schnelle Zwischenstand während einer Umstellung. Der Unterschied liegt in der Absicht: Ein bewusst gesetztes any mit einem Satz Begründung im Kommentar ist eine Entscheidung, ein nebenbei entstandenes ist eine offene Stelle. In den meisten der bewussten Fälle ist unknown übrigens die bessere Wahl, weil es dasselbe entgegennimmt und trotzdem eine Prüfung erzwingt.
Was ist der Unterschied zwischen unknown und einer Zusicherung mit as?
Beide führen dazu, dass der Code weiterläuft, aber nur einer prüft. Eine Zusicherung sagt dem Compiler, er solle dir glauben, und erzeugt keine einzige Zeile Prüfcode. unknown zwingt dich dagegen, vor der Verwendung tatsächlich nachzusehen. Beim Lesen sieht der Unterschied klein aus, zur Laufzeit ist er der zwischen einer Behauptung und einer Feststellung.
Lohnt sich eine Schemabibliothek für ein kleines Projekt?
Sobald du mehr als zwei oder drei Systemgrenzen hast, meistens ja, denn die Alternative ist ein handgeschriebener Typwächter je Struktur, der neben dem Typ gepflegt werden muss. Bei einer einzigen Antwort mit fünf Feldern ist eine ausgeschriebene Prüfung völlig in Ordnung und spart die Abhängigkeit. Die Grenze verläuft nicht an der Projektgröße, sondern an der Zahl der Stellen, an denen fremde Daten hereinkommen.
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.

Vom Notausgang zur geprüften Systemgrenze

In den TypeScript-Kursen bei cmt baust du Typwächter und Schemaprüfungen an echten Antworten auf und siehst, wie weit ein einzelnes any im Code ausstrahlt.