TypeScript
Arquitetura
APIs
tRPC
Type Safety

Durchgängige Typensicherheit: von der Bank bis zum Frontend ohne Bruch an den Grenzen

Der größte Vorteil der End-to-End-Typsicherheit besteht nicht darin, weniger zu tippen, sondern darin, Integrationsfehler zu beseitigen und das Refactoring sicher zu machen. Eine Architekturanalyse.

Die meisten der teuren Fehler, die ich in der Produktion gesehen habe, befanden sich nicht innerhalb einer Ebene. Es war zwischen Schichten. Das Backend hat den Namen eines Feldes geändert und das Frontend wusste es nicht. Die Bankspalte wurde optional und die API behandelte sie weiterhin als obligatorisch. Der Vertrag existierte im Kopf von jemandem, in einem veralteten Dokument oder nirgendwo.

End-to-End-Typsicherheit ist die Idee, diese Lücken zu schließen, indem Typinformationen über alle Systemgrenzen hinweg erfolgen, von der Datenbank bis zur Komponente, die auf dem Bildschirm gerendert wird. Ich möchte dies als eine architektonische Entscheidung diskutieren, mit den tatsächlichen Gewinnen und Kosten, die niemand auf die Folie setzt.

Das Problem ist nicht die Schicht, sondern die Naht zwischen ihnen

Ein typisches System hat mindestens drei Grenzen, an denen der Typ verloren geht. Zwischen der Bank und dem Backend. Zwischen dem Backend und der API. Zwischen der API und dem Frontend. Jede dieser Nähte ist ein Punkt, an dem Wissen über die Form des Stempels weitergegeben werden muss, und dies wird traditionell durch Konvention, Dokumentation oder Glauben weitergegeben.

Wenn die Übertragung fehlschlägt, kann der Compiler nicht helfen, da er jeweils nur eine Seite der Naht sieht. Das Frontend glaubt auf die eine Art, das Backend produziert auf eine andere Art und beide kompilieren problemlos. Der Fehler tritt erst zur Laufzeit auf, wenn die beiden Seiten aufeinandertreffen und feststellen, dass sie unterschiedliche Sprachen sprechen.

Die zentrale These der End-to-End-Typsicherheit ist, dass diese Nähte vom Compiler und nicht von Hope überprüft werden sollten. Wenn das Backend etwas ändert, sollte das Frontend vor dem Commit sofort mit der Kompilierung in seinem eigenen Editor aufhören. Der Integrationsfehler existiert nicht mehr als Kategorie, da er erkannt wird, bevor er auftreten kann.

Der erste Stich: ORMs und typisierte Abfrage-Builder

Alles beginnt bei der Bank. Dort leben die Daten, und dort sollte die Wahrheit über ihre Form ans Licht kommen. Es gibt ORMs und Builder für typisierte Abfragen wie Prisma und Drizzle, sodass der Typ Ihrer Tabellen durch TypeScript bekannt ist, anstatt bei jeder Abfrage zwangsweise ermittelt zu werden.

Prisma verfolgt einen Ansatz, der sich auf sein eigenes Schema konzentriert und aus dem es einen vollständig typisierten Client generiert. Sie beschreiben Ihre Tabellen und Beziehungen in einer speziellen Sprache und Prisma erstellt den Code, der jedes Feld, jeden Typ und jede Beziehung kennt. Abfragen sind geschützt: Die Abfrage einer nicht vorhandenen Spalte führt zu einem Kompilierungsfehler.

Drizzle basiert auf einer anderen Philosophie. Anstelle eines externen Schemas und einer Codegenerierung definieren Sie die Tabellen in TypeScript] selbst und schreiben Abfragen, die wie SQL aussehen, wobei Sie die Typisierung ständig beibehalten. Es liegt näher an der Bank und es gibt weniger magische Schichten zwischen Ihnen und der Abfrage. Für diejenigen, die Wert auf Kontrolle und Vorhersehbarkeit des generierten SQL legen, ist dies normalerweise die bequemste Wahl.

Der philosophische Unterschied spielt bei der Entscheidung eine Rolle, aber der Gewinn ist in beiden Fällen derselbe: Von nun an stammt der Typ Ihrer Daten aus der Datenbank und nicht von einer handgeschriebenen Schnittstelle, deren Aktualisierung jemand vergessen wird.

Die zweite Naht: Typisierte APIs

Die Eingabe der Bank löst ein Drittel des Problems. Die Abfrage weiß, was sie zurückgibt, aber dieses Wissen geht verloren, sobald die Daten in JSON serialisiert und über das Netzwerk gesendet werden. Andererseits erhält das Frontend untypisierten Text und muss erneut raten.

Der klassische Ansatz zur Rekonstruktion des Typs auf der anderen Seite besteht darin, Typen aus einem API-Vertrag zu generieren. Sie beschreiben die API in einem Format wie OpenAPI oder GraphQL, und Tools generieren Clienttypen aus diesem Vertrag. Es funktioniert, und zwar gut in mehrsprachigen Systemen, in denen Frontend und Backend unterschiedliche Sprachen oder separate Teams sind. Der Vertrag ist die Quelle der expliziten Wahrheit, und beide Seiten leiten sich daraus ab.

tRPC geht das gleiche Problem auf einem anderen, radikaleren Weg an. Wenn sich Frontend und Backend beide TypeScript im selben Repository befinden, entfällt der Zwischenvertrag. Der auf dem Server definierte Prozedurtyp wird direkt vom Client abgeleitet, ohne Codegenerierung, ohne separates Schema. Sie rufen eine Funktion im Frontend auf und TypeScript kennt bereits die Parameter und die Rückgabe, da es sich buchstäblich um denselben Typ wie der Server jenseits der Grenze handelt.

Der Effekt ist, dass die Naht zwischen API und Frontend einfach verschwindet. Es muss keine Synchronisierung aufrechterhalten werden, da keine zwei Beschreibungen vorhanden sind. Es hat sich auf dem Server geändert, auf dem Client ist es sofort kaputt gegangen. Es ist dieselbe Single-Source-of-Truth-Logik, die die Datenvalidierung mit Zod](/post/validacao-de-dados-com-zod) so effektiv macht, jetzt auf die Netzwerkgrenze angewendet.

Der wahre Gewinn besteht nicht darin, weniger zu tippen

Hier liegt der Punkt, der diejenigen, die Technologie verstehen, von denen trennt, die ihren Wert verstehen. Das schwache Argument für eine durchgängige Typensicherheit ist die automatische Vervollständigung. Es ist schön, es ist bequem, aber es ist kosmetisch. Wer das als Produktivität beim Tippen verkauft, verkauft das Falsche.

Der eigentliche Gewinn ist die Eliminierung einer ganzen Klasse von Fehlern. Integrationsfehler, die zwischen den Schichten auftreten, können nicht mehr auftreten, da der Compiler sie vor der Laufzeit erkennt. Man korrigiert sie nicht schneller, man schreibt sie einfach nicht. Dies ist ein Wechsel der Kategorie, nicht des Grades.

Der zweite, ebenfalls unterschätzte Vorteil ist das sichere Refactoring. In einem System mit grenzüberschreitenden Typen führt das Umbenennen eines Felds in der Datenbank zu einer Welle von Kompilierungsfehlern bis hin zur letzten Komponente, die es verwendet hat. Sie verfolgen die Fehler wie auf einer Landkarte, und wenn das Projekt erneut kompiliert wird, ist das Refactoring vollständig und korrekt. Ohne dieses Netzwerk ist die Umbenennung eines Feldes ein Akt des Mutes, den niemand tun möchte, und deshalb verrottet der Code: Das Team vermeidet es, das anzufassen, wovor es Angst hat, schweigend zu brechen. Dies ist das zentrale Thema eines Großteils [von fortgeschrittenem TypeScript in der Praxis], und die durchgängige Typsicherheit bringt es bis an die architektonischen Grenzen.

Die Kompromisse, die niemand auf die Folie bringt

Nichts davon ist kostenlos, und wer sich für Architektur entscheidet, muss die Kosten im Auge behalten. Die erste ist die Kopplung. Die direkte tRPC-Typinferenz funktioniert, weil Client und Server gemeinsamen Code haben, was normalerweise ein Monorepo und einen TypeScript-Stack auf beiden Seiten erfordert. Dies verbindet Frontend und Backend auf eine Art und Weise, die genau das sein kann, was Sie in einem kleinen, integrierten Team wollen, oder genau das, was Sie nicht wollen, wenn sich Teams unabhängig weiterentwickeln müssen.

Das zweite ist Lock-in. Aufbauend auf tRPC setzen Prisma oder Drizzle auf diese Tools und ihre Ökosysteme. Eine spätere Migration ist teuer, und die Abstraktion, die Sie heute schützt, ist dieselbe, die Sie morgen zurückhält. Daher ist die Wahl zwischen einer vertragstypisierten API, die portabler und sprachunabhängiger ist, und einer Inferenz-API, die produktiver, aber stärker gekoppelt ist, eine strategische Entscheidung und kein technisches Detail.

Der dritte Punkt ist die Lernkurve und die Schemakosten. Das Modellieren von Schemata, das Verstehen von Schlussfolgerungen und das Diagnostizieren von Typfehlern ist eine echte Komplexität, die manchmal langwierig und einschüchternd ist. Ältere Teams absorbieren schnell; Mannschaften in Formation spüren die Reibung. Diese Entscheidungen werden in Monorepos mit TypeScript noch dichter, wo die gemeinsame Typisierung die größte Stärke und auch die größte Quelle für Build-Komplexität darstellt.

Meine Empfehlung ist pragmatisch: Wenn Sie einen End-to-End-TypeScript-Stack haben, ein Team, das Wert auf Iterationsgeschwindigkeit legt und Kopplungen toleriert, liefert direkte Inferenz eine unverhältnismäßige Rendite. Wenn Sie unabhängige Teams, mehrere Sprachen oder externe Kunden haben, die Ihre API nutzen, bevorzugen Sie den expliziten Vertrag und zahlen die Kosten für die Typgenerierung im Austausch für die Portabilität.

Bevor Sie sich für das Tool entscheiden, legen Sie Ihre Grenzen fest und entscheiden Sie jeweils, wie viel Kopplung Sie bereit sind, für Sicherheit einzutauschen. Dieses frühzeitig geführte Gespräch ist mehr wert als jeder Framework-Benchmark.

Lesen Sie auch