TypeScript
Qualidade de Software
Arquitetura
Boas Práticas
Liderança Técnica

Fortgeschrittenes TypeScript in der Praxis: Was die oberflächliche Nutzung von der ausgereiften Nutzung unterscheidet

Bei reifem TypeScript geht es nicht darum, mehr Typen zu schreiben, sondern darum, den Compiler zu Ihren Gunsten arbeiten zu lassen.

Die Übernahme von TypeScript ist einfach. Eine gute Nutzung ist selten. Der Unterschied zwischen den beiden Dingen zeigt sich nicht am ersten Tag, sondern sechs Monate später, wenn das Team eine Umgestaltung durchführen und herausfinden muss, ob es sich bei den Typen um Verbündete oder um Dekoration handelt.

Es gibt eine Oberfläche TypeScript, in der alles manuell annotiert wird, any taucht immer dann auf, wenn es schwierig wird, und der Compiler dient nur der automatischen Vervollständigung von Namen. Und es gibt ein ausgereiftes TypeScript, bei dem die Typen tatsächlich das Problem modellieren und der Compiler zu einem unermüdlichen Prüfer wird, der Fehler erkennt, bevor der Code ausgeführt wird.

In diesem Text geht es um den zweiten. Nicht als Tutorial, sondern als Argument über den Wert jeder Ressource für die Gesundheit des Codes und des Teams.

Schlussfolgerung: Schreiben Sie weniger, um mehr sicherzustellen

Der erste Instinkt eines jeden, der sich mit TypeScript beschäftigt], ist, alles aufzuschreiben. Jede Variable, jede Rückgabe, jeder Parameter erhält einen handschriftlichen Typ. Das scheint eifrig zu sein und in der Praxis ist das Gegenteil der Fall.

Übermäßige manuelle Anmerkungen erzeugen Lärm und, schlimmer noch, Lügen. Der handgeschriebene Typ kann vom tatsächlichen Wert abweichen, und dann haben Sie eine Anmerkung, die eine Absicht dokumentiert, die der Code nicht mehr erfüllt. Inferenz lügt nicht: Sie leitet den Typ aus dem Faktenwert ab.

Bei der ausgereiften Verwendung vertraut man der Schlussfolgerung dort, wo sie zuverlässig ist, und reserviert Anmerkungen dort, wo sie wichtig sind, also an den Grenzen. Öffentliche Funktionssignatur, Modulvertrag, Datenformat, das in den Systemrand gelangt. Lassen Sie im Wesentlichen den Compiler schließen. Weniger Code, weniger Meinungsverschiedenheiten, mehr Wahrheit.

Generics: Der Unterschied zwischen der Wiederverwendung von Typen und dem Verlust von Typen

Generika sind oft beängstigend, weil die Syntax akademisch wirkt. Das Konzept ist einfach und äußerst praktisch: Es geht darum, wie man etwas Wiederverwendbares schreibt, ohne dabei Typinformationen wegzuwerfen.

Ohne Generika besteht der einfachste Ausweg für eine Funktion, die mit allem funktioniert, darin, die Ein- und Ausgabe als zu generisch einzugeben, was zur Folge hat, dass der Typ verloren geht. Wer die Funktion aufruft, erhält einen formlosen Wert zurück und muss raten, was er damit machen soll.

Bei Generika bleibt die Beziehung zwischen Eingabe und Ausgabe erhalten. Eine Funktion, die eine Liste von etwas empfängt, gibt dasselbe zurück, und der Compiler weiß dies zum Zeitpunkt des Aufrufs. Dies ist das Herzstück der sicheren Wiederverwendung: Verhalten abstrahieren, ohne Informationen zu abstrahieren. Teams, die Generika beherrschen, schreiben weniger doppelte Dienstprogramme und erhalten in allen Dienstprogrammen eine korrekte automatische Vervollständigung.

Dienstprogrammtypen: Format wiederverwenden, ohne das Format zu wiederholen

Jedes System akkumuliert Typen, die Variationen anderer Typen sind. Die Teilversion eines Objekts für ein Formular, die Version ohne Passwortfeld zum Senden an den Client, die schreibgeschützte Version einer Konfiguration.

Der naive Ansatz besteht darin, jede Variation manuell neu zu deklarieren. Das Problem tritt auf, wenn sich der ursprüngliche Typ ändert: Es müssen jetzt fünf Kopien aktualisiert werden und es gibt keine Garantie dafür, dass Sie sich alle gemerkt haben. Es ist die gleiche Falle wie duplizierter Code, nur in Typform.

Utilitaristische Typen lösen dieses Problem, indem sie eine Form von einer anderen ableiten. Wenn sich der Basistyp ändert, folgen die Variationen von selbst, da sie darauf basierend definiert wurden. Dadurch werden die Typen zu einem Single Point of Truth und eliminiert die subtile Klasse von Fehlern, bei denen der Code und sein Typ unterschiedlich schnell aktualisiert wurden.

Eingrenzung: Bringen Sie dem Compiler bei, mit Ihnen zu argumentieren

Die vielleicht am meisten unterschätzte Funktion ist die Eingrenzung, also die Fähigkeit von TypeScript, einen Typ im Verlauf des Codeflusses einzugrenzen. Sie prüfen, ob ein Wert vorhanden ist, und innerhalb dieses Blocks beginnt der Compiler, ihn als vorhanden zu behandeln.

Dies verändert die Art und Weise, wie das Team mit dem Randfall umgeht. Anstatt überall Abwehrkontrollen zu verteilen und zu hoffen, modellieren Sie die möglichen Zustände und lassen den Compiler verlangen, dass jeder Zustand behandelt wird. Der Wert kann das eine oder andere sein und der Code wird nur kompiliert, wenn beide Pfade abgedeckt wurden.

Der kulturelle Effekt davon ist groß. Die Null ist keine Produktionsüberraschung mehr und wird zu einer Voraussetzung für die Schreibzeit. Das Team entdeckt nicht mehr, dass es einen Fall vergessen hat, wenn der Kunde sich beschwert, und beginnt, ihn zu entdecken, wenn der Redakteur sich beschwert. Es ist der Unterschied zwischen einem Fehler und einer Erinnerung.

Der Krieg gegen jeden

any ist das Escape-Ventil von TypeScript, und wie jedes Escape-Ventil ist es in minimalen Dosen notwendig und in normalen Dosen destruktiv. Ein any schaltet nicht nur den Typ dieser Variablen aus. Es verunreinigt alles, was es berührt, denn alles, was von einem any abgeleitet ist, wird auch zu einem any.

Eine Basis mit einer Verteilung von any hat das Schlimmste aus beiden Welten: Sie zahlt die Kosten für die Konfiguration und Wartung von TypeScript, verliert aber die Garantien genau an den riskantesten Stellen, nämlich dort, wo jemand nicht tippen konnte und aufgab.

Reife Teams betrachten any als Warnzeichen, nicht als Lösung. Wenn der Typ wirklich unbekannt ist, besteht die sichere Möglichkeit, ihn als unbekannt zu markieren und vor der Verwendung eine Überprüfung zu erzwingen, anstatt alles freizugeben. Die Faustregel ist einfach: any sollte eine seltene, gerechtfertigte und in der Überprüfung sichtbare Ausnahme sein, niemals der Standardausweg aus einem Problem.

Modellierung der Domäne: der Sprung, der sich am meisten auszahlt

Die höchste Stufe der TypeScript]-Nutzung ist nicht technischer Natur, sondern Design. Dabei werden Typen verwendet, um Geschäftsregeln so zu beschreiben, dass der ungültige Zustand einfach nicht existieren kann.

Eine Bestellung, die bezahlt oder ausstehend sein kann, aber niemals beides. Ein Benutzer, der im eingeladenen Zustand noch nicht über bestimmte Daten verfügt, im aktiven Zustand jedoch zwangsläufig über bestimmte Daten verfügt. Wenn Sie diese Zustände als unterschiedliche Typen modellieren, verhindert der Compiler unmögliche Kombinationen, bevor Tests ausgeführt werden.

Dies verändert die Diskussion im Team. Anstelle von „Kann dieses Feld hier leer sein“ ist die Antwort typisiert, explizit und überprüfbar. Die Domäne wird im ausgeführten Code dokumentiert, nicht in einem Dokument, das niemand aktualisiert. Für den Rand des Systems, wo externe Daten ohne Garantie ankommen, lohnt es sich, dies mit einer Laufzeitvalidierung zu kombinieren, und der natürliche Weg ist Datenvalidierung mit Zod, die die realen Daten mit dem Typ verbindet.

Wenn man alles zusammenzählt, ist die Rendite nicht ästhetisch, sondern operativ. Weniger Fehler gelangen in die Produktion, da der Compiler sie früher erkennt. Refactoring, dem das Team ohne Angst gegenübersteht, da es sofort sichtbar ist, dass etwas kaputt geht. Dokumentation, die nicht altert, weil sie der Code selbst ist.

Bei Mature TypeScript geht es nicht darum, mehr Typen zu schreiben. Es geht darum, die richtigen Typen an den richtigen Stellen zu schreiben und den Compiler die langweilige Arbeit der Konsistenzprüfung erledigen zu lassen, also genau die Arbeit, die Menschen schlecht und Maschinen gut erledigen.

Wenn Ihr Team bereits TypeScript verwendet, Typen aber immer noch als Bürokratie behandelt, besteht der nächste Schritt darin, die Messlatte für die Codeüberprüfung höher zu legen und die Typqualität als Teil der Codequalität zu behandeln. Um zu sehen, wie sich diese Konzepte in eine größere Architektur integrieren lassen, lohnt es sich, einen Blick auf Monorepos mit TypeScript zu werfen.

Lesen Sie auch