1 Punkte von GN⁺ 2023-07-09 | 1 Kommentare | Auf WhatsApp teilen
  • 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

 
GN⁺ 2023-07-09
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.

    • Es geht wohl weniger darum, Runtime-Typsicherheit an sich zu fordern, sondern eher darum, Typen zur Compile-Zeit per Reflection auszuwerten, Werte zu erzeugen und diese Informationen dann zur Laufzeit zu nutzen.
      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 dem T zur Laufzeit als Argument übergeben und verglichen wird, aber innerhalb der aktuellen TypeScript-Philosophie erscheint das schwierig.
    • Schon anhand des Titels war sofort klar, dass es um eine Funktion geht, die ich mir schon lange wünsche; es kommt eher einer Forderung nach einem offizielleren und besser unterstützten https://github.com/rbuckton/reflect-metadata nahe.
      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 Reflect hinterlegen, 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-Proxy gewisse Prüfungen ergänzen, und wenn Typinformationen zur Laufzeit erhalten blieben, würden sich als ausführbare Metadaten viele interessante Möglichkeiten eröffnen.
    • Dass es unmöglich sei, weil TypeScript ein Compiler ist, stimmt nicht. Es würde reichen, eine Reflection-Bibliothek zu unterstützen, die Typinformationen als JS-Objekte ausgibt und sie zur Laufzeit abfragbar macht.
      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.
    • Schon beim groben Überfliegen war die Forderung klar. TypeScript soll die Typinformationen, die beim Type Erasure ermittelt wurden, über einen zusätzlichen Kanal neben dem erzeugten JavaScript ausgeben.
      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.
    • Ganz oben im README ist ein „7 Jahre altes GitHub-Issue“ verlinkt, das das Problem direkter erklärt.
      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.

    • Ich programmiere noch nicht lange und frage mich, warum TypeScript keine ausgefeilteren benutzerdefinierten Datentypen definieren kann.
      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.
    • Ein offizielles Preprocessor-Plugin für den TypeScript-Compiler könnte hilfreich sein.
      Schon heute kann, wer es braucht, einen Preprocessor bauen, der vor der Übergabe an tsc aus 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 Microsoft MacroScript als TypeScript-Plugin oder als Top-Level-Wrapper hosten würde, könnte das dieses Problem lösen.
      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.
    • Mich würde interessieren, was ihr von dem anderen Vorschlag haltet, Typinformationen nach der Kompilierung auf 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.

    • Nicht unbedingt. Wenn etwas wie ein Makro generateTypeInfo!() zu einem JS-Objekt expandiert, das den Typ Foo kodiert, 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 Foo tatsä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.
    • Man kann beides haben. Man kann sich eine Welt wie bei der Homoikonizität von Lisp vorstellen, in der Code und Daten eng beieinanderliegen.
      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.
    • Ich verstehe diese Logik nicht so recht. TypeScript ist bereits eine neue Superset-Sprache, die nach JS kompiliert wird.
      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.
    • In ausreichend großen Projekten sind dynamisch typisierte Sprachen nicht gut und eher eine verstrickte Katastrophe, bei der man ständig über unerwartete Werte wie NaN stolpert.
      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.
    • TypeScript ist nicht einfach nur JavaScript mit Typannotationen. Das kann ein Modus sein, aber es kann auch ältere JS-Konstrukte ausgeben, sodass es ganz anders aussehen kann als der ursprüngliche TS-Code.
      Es ist eher Typannotationen + Babel, und noch eine sehr nützliche Funktion hinzuzufügen ist eine gute Idee. Dass man einen mit JSON.parse eingelesenen 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 Enum ein 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.

    • Es gibt außer Enum noch weitere Ausnahmen, die Runtime-Code ausgeben, und die meisten davon gelten ebenfalls als Fehler. Besonders module und namespace waren 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.
    • Die Aussage „TS-Code wird ohne Transformation zu JS“ ist nur möglich, wenn man die gesamte Sprache und die Typschreibweise so betrachtet.
  • 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-ts oder zod und 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.

    • Nicht perfekt, aber ich bin ziemlich zufrieden mit dem Ansatz, in zod Schemas 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 in ts-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.
    • Ob so eine Funktion wirklich in JavaScript aufgenommen werden sollte, ist sehr fraglich.
      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 enum nicht eingebaut, wenn er noch einmal von vorn anfangen würde. Denn enum ist die einzige Funktion, die Runtime-Code ausgibt.
    TypeScript ändert das Runtime-Verhalten nicht, und es gibt auch kein spezielles TypeScript-eigenes {#if} oder Ähnliches.

    • Das stimmt. Ich weiß nicht, ob das der einzige Grund war, aber es war sicher einer der Gründe.
      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 die this-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#emitDecoratorMetadata

  • Der 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 enum wird 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 komplette tsc in 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

    • Es gibt viele Möglichkeiten, Runtime-Typinformationen statisch und ohne Runtime zu exportieren. Man könnte keyof in eine Liste von Klassenschlüsseln umwandeln oder TS-Klassen dazu bringen, Schlüssel wie ES6-Klassen mit undefined zu initialisieren
      Derzeit entfernt eine TypeScript-Klasse alle nicht explizit definierten Schlüssel, sodass Object.keys() bei einer neuen Instanz nichts zurückgibt. Schon eine tsconfig.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 machen
  • Ich 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

    • Der Zweck von TypeScript war immer, im Web zu laufen, nicht sprachliche Überlegenheit gegenüber C# zu bieten
      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

    • Nominale Typen haben einen anderen Einsatzzweck
      Ein einfaches Beispiel ist, dass man in einer Domain die Zahl 2 von einem Geldbetrag 2 unterscheiden können muss