- Es wird die Notwendigkeit betont, dass TypeScript Runtime-Typinformationen ausgibt, und eine Liste von Type Mapping-, Code Generation-/External Tool- und Adapter-Projekten gesammelt, die dieses Problem umgehen
- Das Kernproblem ist, dass bei der Behandlung von Serialisierung und Validierung ohne ein reflektives Typsystem endloser Boilerplate-Code oder maßgeschneiderte Code-Generierung auf Basis von Schema-Dateien erforderlich ist
- Als Workarounds werden io-ts und zod genannt; dabei ist es umständlich, Typen in der jeweils bibliotheksspezifischen Weise erneut deklarieren zu müssen, und die Bibliotheken unterstützen nicht alle Typ-Features von TypeScript
- Es wird anerkannt, dass die Typlöschung von TypeScript den Vorteil hat, dass JavaScript-Projekte das ausgegebene JavaScript ohne TypeScript-Kenntnisse verwenden können; zugleich wird argumentiert, dass Typinformationen in Form einer vom Code getrennten Lookup-Tabelle ausgegeben werden könnten
- Mit der Bitte, das Problem nicht per Decorators zu lösen, werden Ansätze wie eine vom Compiler erkannte Higher-Order-Funktion wie
typescript.generateRuntimeType<T>(), F# Type Providers und C# Source Generators vorgeschlagen, um die Verwendung von Interfaces und die Unterstützung externer Bibliothekstypen zu ermöglichen - Als bestehende verwandte Diskussion wird ein 8 Jahre altes GitHub-Issue verlinkt, verbunden mit der Bitte, bei Projekten mit demselben Problem unabhängig von der Größe per PR eine Ergänzung zur Liste einzureichen
1 Kommentare
Hacker-News-Kommentare
Ich musste die Problembeschreibung etwa viermal lesen, um zu verstehen, was überhaupt gefordert wird, und gerade so ein Fall zeigt, warum prägnantes Schreiben wichtig ist.
Die eigentliche Forderung scheint Runtime-Typsicherheit zu sein, aber das wirkt eher unwahrscheinlich, weil TypeScript stets eine klare Grenze gezogen hat, kein Ersatz bzw. zusätzliches Runtime für JavaScript sein zu wollen. TypeScript kompiliert nach JS und ist dann aus dem Spiel; was danach in V8 und anderswo passiert, liegt außerhalb seines Zuständigkeitsbereichs.
Die Forderung „TypeScript sollte Runtime-Typinformationen ausgeben“ kommt eher der Forderung nach einem neuen Produkt gleich, das sich deutlich vom heutigen TypeScript unterscheidet.
Zum Beispiel wäre es nützlich, eine generische
validate-Funktion bauen zu können, die ein beliebiges Interface und ein Objekt entgegennimmt und dieses validiert. Denkbar wären Compile-Time-Reflection, die typabhängigen JS-Validierungscode erzeugt, oder ein Ansatz, bei demTzur Laufzeit als Argument übergeben und verglichen wird, aber innerhalb der aktuellen TypeScript-Philosophie erscheint das schwierig.TypeScript kennt während der Kompilierung viele Typinformationen, verwirft sie aber nach Abschluss der Kompilierung. Diese Informationen ließen sich in eine Datei ausgeben oder als Metadaten in
Reflecthinterlegen, und dass sie trotzdem verworfen werden, ist besonders mit Blick auf den JavaScript-Charakter „alles ist ein Objekt“ eine bedauerliche Einschränkung.Selbst wenn man damit keine vollständige TS-Typsicherheit bekäme, könnte man über Getter/Setter oder ES2015-
Proxygewisse Prüfungen ergänzen, und wenn Typinformationen zur Laufzeit erhalten blieben, würden sich als ausführbare Metadaten viele interessante Möglichkeiten eröffnen.Schon heute werden TS-
enums als JS-Objekte ausgegeben und sind zur Laufzeit abfragbar, String-Literal-Union-Types dagegen nicht. Außerdem unterstützt TypeScript Type-Guard-Funktionen, daher scheint es auch nicht besonders schwer, solche Funktionen automatisch aus Informationen im Typsystem zu erzeugen.Man kann das mit PDB-Dateien vergleichen. Es ist Information, die TypeScript bereits besitzt und dann wegwirft, daher braucht es dafür kein völlig neues Produkt.
https://github.com/microsoft/TypeScript/issues/3628
Aus Sicht eines TypeScript-PMs ist der Wunsch nachvollziehbar. Für Datenvalidierung braucht man oft Runtime-Typprüfungen, und es gibt viele Bibliotheken, die genau diese Lücke füllen wollen.
Dass es jedoch viele Bibliotheken mit unterschiedlichen Designentscheidungen gibt, ist selbst schon ein Zeichen dafür, dass dieses Problem keine klar richtige, bereits gelöste Antwort hat. Man wusste das schon beim ursprünglichen Design von TypeScript, und dieses Prinzip hat sich meines Erachtens gut gehalten.
Stattdessen ist TypeScript inzwischen stark genug geworden, um das tatsächliche Verhalten von Runtime-Typprüfungsbibliotheken typseitig präzise auszudrücken, und Nutzer können über APIs aus Typen Runtime-Validierungslogik aufbauen. Das wirkt auf mich wie ein vernünftiges Maß an Flexibilität.
Man könnte sich zum Beispiel eine Sprache vorstellen, in der Zahlen innerhalb eines bestimmten Bereichs oder Strings, die einem Postleitzahlenmuster entsprechen, als Typen definiert werden und der Compiler ihre Gültigkeit mithilfe von Validierungsfunktionen prüft, die wie normale Funktionen geschrieben sind. Ich frage mich, ob das Fehlen solcher Funktionen eine Designentscheidung zur Vermeidung unnötiger Komplexität ist oder eher auf technische Einschränkungen wie Performance zurückgeht.
Schon heute kann, wer es braucht, einen Preprocessor bauen, der vor der Übergabe an
tscaus Typinformationen Runtime-Objekte erzeugt, aber dieser Aufwand ist derzeit zersplittert und uneinheitlich. Mit einem offiziellen pluginfähigen Preprocessor und einem entsprechenden Ökosystem könnte sich eine gute Lösung finden.Wenn man nur den Anstoß gibt, würde die Community wahrscheinlich bei der Wartung helfen, und damit ließen sich verschiedenste Anforderungen an Codegenerierung erfüllen, von der Client-Generierung bis zu Runtime-Type-Assertions.
Class-Objekten zu belassen.Dafür gibt es einen Grund. TypeScript würde so etwas wie eine Runtime über JavaScript werden und sich in eine neue Sprache verwandeln, die nach JS kompiliert wird.
Im Moment ist TypeScript eher JavaScript mit Typannotationen. Es gibt bereits viele Sprachen, die nach JS kompilieren; dann kann man auch eine davon verwenden. Wer TypeScript mit Runtime-Typen möchte, scheint eher Java-/OOP-artigen Code als JavaScript schreiben zu wollen, aber JavaScript ist eine dynamisch typisierte Sprache, und auch das ist ein Vorteil.
generateTypeInfo!()zu einem JS-Objekt expandiert, das den TypFookodiert, wird immer noch zu lesbarem JavaScript kompiliert.Allerdings bricht dieses Makro die Eigenschaft, dass „TS = JS mit Typannotationen ist und Kompilierung nur das Entfernen der Annotationen bedeutet“. Denn man muss die strukturellen Typen von
Footatsächlich berechnen.Außerdem sind TypeScript-Typen strukturell, daher kann man zur Laufzeit durch Prüfen der Objektstruktur den Typ bis zu einem gewissen Grad erkennen. Sobald man aber ausgelöschte nominale Information oder Typnamen verlangt, etwa ob etwas zu einer String-Union gehört, wird es wegen der Turing-Vollständigkeit des TypeScript-Typsystems und impliziter struktureller Umwandlungen schnell schwierig.
Ernsthaft gesagt: Selbst ohne Runtime kann man Reflection bekommen, wenn man Typen als Daten zugänglich macht. Wenn man sieht, wie viele Entwickler versuchen, das nachzuahmen, wirkt es eher töricht, es nicht zu tun.
Runtime-Typen lösen das Problem, für das Typsystem des Compilers und für Datenvalidierung zwei getrennte Typsysteme doppelt pflegen zu müssen, und das scheint nicht direkt mit OOP zusammenzuhängen.
io-ts, eine bevorzugte Ausweichbibliothek für dieses Problem, stützt sich auch stark auf funktionale Programmierung.NaNstolpert.In den Workflows, die ich gesehen habe, ist TypeScript bereits eine Sprache, die nach JS kompiliert, daher sollte man diesen Vorteil möglichst umfassend nutzen.
Es ist eher Typannotationen + Babel, und noch eine sehr nützliche Funktion hinzuzufügen ist eine gute Idee. Dass man einen mit
JSON.parseeingelesenen String nicht sicher in eine typisierte Struktur umwandeln kann, ist seltsam, und andere Sprachen können das meistens.Vor allem Folgenden: Das ist ein legitimes Thema, über das man gut diskutieren kann, und ich glaube nicht, dass es objektiv genau eine richtige Antwort gibt.
TypeScript ist eine optionale Schicht über JavaScript, abgesehen von der Ausnahme, dass
Enumein Objekt ausgibt, und TS-Code wird zu JS, wenn man nur die Typen entfernt. Solange man dieses Prinzip nicht deutlich verlässt, ähnelt die eigentliche Forderung eher einer Bibliothek, die aus Typen Serializer/Validatoren erzeugt. Solche Bibliotheken gibt es bereits viele, und letztlich wirkt es wie die Forderung, eine davon zur offiziellen Standardoption zu machen.Persönlich möchte ich nicht, dass Runtime-Reflection in den TypeScript-Sprachkern kommt. Zur Runtime soll einfach nur JavaScript existieren, und ich schätze es sehr, dass das ausgegebene JS auch ohne Source Maps lesbar und debuggbar bleibt.
Enumnoch weitere Ausnahmen, die Runtime-Code ausgeben, und die meisten davon gelten ebenfalls als Fehler. Besondersmoduleundnamespacewaren Runtime-Strukturen.Allerdings wurden sie in den letzten Jahren vor allem innerhalb von TypeScript selbst verwendet, und TypeScript entfernt sich inzwischen auch davon. Außerdem gibt es als recht populäre Ausnahme Parameter Properties: eine Syntax, mit der man in Konstruktorparametern Typen für Klassenmitglieder definiert. Sie reduziert doppelten Boilerplate-Code und ist deshalb wohl weniger umstritten.
Statt die TypeScript-Götter anzuflehen, wäre es vielleicht besser, auf der JavaScript-Seite einen Deal auszuhandeln und Typprüfung in JS einzubauen.
Wenn man Runtime-Typen braucht, verwendet man Type Guards; wenn man sie oft braucht, nimmt man
io-tsoderzodund schreibt Typen als Validatoren/Codecs/Schemata. Bis es in JavaScript eine abgestimmte Vorgehensweise gibt, sollte nicht die TS-Spezifikation, der Type Checker und die Community die Last der Runtime-Validierung tragen müssen.zodSchemas und Validatoren zu schreiben und daraus Typen abzuleiten. Gut ist, dass sich die Validierung zusammen mit Typänderungen weiterentwickelt.Manche finden, so etwas sollte in die Sprache selbst, aber Bibliotheken enthalten viele umstrittene Designentscheidungen, daher kann es sogar besser sein, eine Implementierung passend zu den Projektanforderungen auszuwählen.
Allerdings passen Muster wie
zod-Runtime-Validierung und schema-basierte Typableitung nicht immer gut zu Pattern Matching ints-pattern. Die Typdefinitionen solcher Bibliotheken sind labyrinthartig, und manchmal funktioniert Code, der funktionieren sollte, nicht wie erwartet. Es wäre großartig, wenn Runtime-Sicherheit und Pattern Matching mit Vollständigkeitsprüfung sich nahtlos kombinieren ließen.Bei
Promise, Dekoratoren und dem neuen Pipe-Operator habe ich alle das Gefühl, dass die Implementierungsrichtung falsch gewählt wurde.Ich meine mich zu erinnern, dass einer der TypeScript-Entwickler sagte, er hätte
enumnicht eingebaut, wenn er noch einmal von vorn anfangen würde. Dennenumist die einzige Funktion, die Runtime-Code ausgibt.TypeScript ändert das Runtime-Verhalten nicht, und es gibt auch kein spezielles TypeScript-eigenes
{#if}oder Ähnliches.Allerdings gibt es in TypeScript einige Funktionen, die über „JavaScript + Typannotationen“ hinausgehen.
namespace, die Syntax zum Definieren von Klassen-Properties über Konstruktorargumente, alte experimentelle Dekoratoren und diethis-Parameter-Syntax, die beim Kompilieren verschwindet, aber nach mehr aussieht als bloß Typannotationen an JS-Funktionen.Ein besserer Titel wäre eher „TypeScript, bitte gib uns Reflection/Runtime-Typen“.
Die derzeit beste Lösung ist wahrscheinlich
emitDecoratorMetadata. https://www.typescriptlang.org/tsconfig#emitDecoratorMetadataDer Verfasser scheint das Designziel von TypeScript misszuverstehen. Das Ziel ist nicht, sauberes JS ohne Komplexität zu erzeugen, sondern sicherzustellen, dass die Runtime-Semantik von TypeScript identisch zu JavaScript bleibt
Mit der bedauerlichen Ausnahme von
enumwird TypeScript durch bloßes Entfernen der Typannotationen zu JavaScript. Das gesamte Ökosystem rund um TypeScript baut auf vollständiger Type Erasure auf, und wenn diese Forderung akzeptiert würde, könnte TS-Unterstützung in ESBuild, Deno und Bun nahezu unmöglich werden. Denn jedes davon müsste das komplettetscin seiner eigenen Sprache neu implementieren.Die komplexen Bibliotheken, über die sich der OP beschwert, sind dagegen im User Space implementiert und deshalb mit solchen Tools kompatibel
keyofin eine Liste von Klassenschlüsseln umwandeln oder TS-Klassen dazu bringen, Schlüssel wie ES6-Klassen mitundefinedzu initialisierenDerzeit entfernt eine TypeScript-Klasse alle nicht explizit definierten Schlüssel, sodass
Object.keys()bei einer neuen Instanz nichts zurückgibt. Schon einetsconfig.json-Option, die TS-Klassen wie ES6-Klassen transformiert, oder einfach die gemeinsame Nutzung von ES6-Klassen in TS würde die Codegenerierung deutlich erleichtern.Außerdem wäre eine statische Syntax wie
tstypeof Foo::bar, die zu"string"kompiliert, großartig. Schon grundlegende RTTI könnte sofort viel unsauberen Boilerplate-TypeScript-Code überflüssig machenIch freue mich über diesen Beitrag. Ich habe 2018 mit TypeScript angefangen und nach etwa zwei Jahren begonnen zu glauben, dass es im Kern keine vollständige Lösung ist
Ich komme aus Haskell/C#/F#, und trotz seines mächtigen Typsystems bietet TS außer in bestimmten Teilen der Entwicklung nicht viele der Vorteile, die diese Sprachen mitbringen. Beim Umgang mit der realen Welt ist es in der Praxis deutlich eingeschränkter als C#. Wenn man nicht wüsste, dass es nach dem Kompilieren ungeprüft zu JS wird, müsste man bei einem Tool, das statische Typisierung beansprucht, eigentlich keine Bugs vermeiden müssen, die nur wegen leaky abstractions möglich sind
Wenn man .NET entwickeln muss, nimmt man C#, und wenn man Web entwickeln muss, nimmt man TypeScript. C# ist nominal typisiert und hat recht brauchbare reifizierte Generics, während TypeScript strukturell typisiert ist und zugunsten eines mächtigen Typsystems für komplexe Typbeziehungen auf Soundness verzichtet.
Ob eine Sprache, die beides verbindet, gut wäre, ist fraglich; beide Sprachen funktionieren in unterschiedliche Richtungen gut, aber diese Richtungen passen nicht besonders gut zusammen
Für die Validierung von Runtime-Daten war ich mit https://zod.dev vollkommen zufrieden
Dass man keine separaten nominalen Typen definieren muss und die eigene Absicht direkt an Ort und Stelle mit einer fluenten API ausdrücken kann, fand ich ziemlich großartig
Ein einfaches Beispiel ist, dass man in einer Domain die Zahl
2von einem Geldbetrag2unterscheiden können muss