1 points par GN⁺ 2023-07-09 | 1 commentaires | Partager sur WhatsApp
  • Une liste qui défend la nécessité pour TypeScript d’émettre des informations de type à l’exécution, et regroupe les contournements de ce problème comme le Type Mapping, la génération de code / les outils externes et les projets d’adaptation
  • Le problème central est que, sans système de types réflexif, la sérialisation et la validation exigent soit une quantité infinie de boilerplate, soit une génération de code sur mesure fondée sur des fichiers de schéma
  • Des solutions de contournement comme io-ts et zod sont proposées, mais elles imposent de redéclarer les types selon l’approche propre à chaque bibliothèque, et ces bibliothèques ne prennent pas en charge toutes les fonctionnalités de type de TypeScript
  • L’effacement des types de TypeScript est reconnu comme un avantage, car il permet aux projets JavaScript d’utiliser le JavaScript émis sans connaissance de TypeScript, mais l’auteur soutient qu’il serait possible d’émettre les informations de type sous la forme d’une table de correspondance séparée du code
  • Avec la demande de ne pas résoudre cela via des décorateurs, le texte propose des approches comme une fonction d’ordre supérieur reconnue par le compilateur telle que typescript.generateRuntimeType<T>(), ainsi que des méthodes comparables aux F# Type Providers et aux C# Source Generators, afin de permettre l’usage des interfaces et la prise en charge des types de bibliothèques externes
  • Il renvoie à une issue GitHub vieille de 8 ans parmi les discussions existantes sur le sujet, et demande que les projets ayant rencontré le même problème envoient une PR pour être ajoutés à la liste, quel que soit le nombre d’étoiles

1 commentaires

 
GN⁺ 2023-07-09
Avis de Hacker News
  • Il m’a fallu lire l’énoncé du problème environ 4 fois pour comprendre ce qui était demandé, et ce genre de cas montre bien pourquoi écrire de façon concise est important
    La demande réelle semble être la sûreté de typage à l’exécution, mais TypeScript a toujours tracé une ligne claire en disant qu’il ne deviendrait pas un runtime de remplacement/complémentaire à JavaScript, donc cela paraît peu probable. Le rôle de TypeScript est de compiler vers JS puis de s’effacer ; ce qui se passe ensuite dans V8, etc., est hors périmètre.
    Demander que « TypeScript émette les informations de type à l’exécution » revient presque à demander un nouveau produit assez différent de TypeScript tel qu’il existe aujourd’hui

    • Plutôt que de demander la sûreté de typage à l’exécution elle-même, cela ressemble davantage à une demande de réflexion (reflection) des types au moment de la compilation pour générer des valeurs et permettre d’utiliser ces informations à l’exécution
      Par exemple, l’idée serait de pouvoir créer une fonction générique validate qui accepte une interface et un objet arbitraires et les valide. Avec de la réflexion à la compilation, on pourrait générer du code JS de validation propre à chaque type, ou passer T comme argument à l’exécution pour comparer, mais cela me semble difficile dans la philosophie actuelle de TypeScript
    • Rien qu’au titre, j’ai tout de suite compris que c’était une fonctionnalité que je voulais depuis longtemps, et cela ressemble plutôt à une demande pour quelque chose comme https://github.com/rbuckton/reflect-metadata, mais plus officiel et mieux pris en charge
      TypeScript connaît beaucoup d’informations de type pendant la compilation, puis les jette une fois celle-ci terminée. Il pourrait exporter ces informations dans un fichier ou les laisser sous forme de métadonnées dans Reflect, et les jeter reste une limitation regrettable, surtout quand on pense au caractère « tout est objet » de JavaScript.
      Même sans obtenir une sûreté de typage TS exacte, on pourrait ajouter certaines vérifications avec des getter/setter ou des Proxy ES2015 ; et conserver les informations de type à l’exécution ouvrirait plusieurs possibilités intéressantes sous forme de métadonnées exécutables
    • Dire que c’est impossible parce que TypeScript est un compilateur n’est pas correct. Il suffirait de prendre en charge une bibliothèque de réflexion qui émet les informations de type sous forme d’objets JS et les consulte à l’exécution
      Les enum TS sont déjà émis sous forme d’objets JS et peuvent être consultés à l’exécution, contrairement aux types union de littéraux de chaînes. TypeScript prend aussi en charge les fonctions de garde de type ; générer automatiquement ce genre de fonctions à partir des informations du système de types ne semble donc pas particulièrement difficile
    • Même en parcourant rapidement, la demande était claire. Il s’agit de demander à TypeScript d’émettre, via un canal auxiliaire à côté du JavaScript généré, les informations de type qu’il a obtenues pendant l’effacement des types
      On peut penser à une analogie avec les fichiers PDB. Comme ce sont des informations que TypeScript possède déjà puis jette, cela ne nécessite pas un produit entièrement nouveau
    • En haut du README, il y a un lien vers une « issue GitHub vieille de 7 ans », où le problème est expliqué plus directement
      https://github.com/microsoft/TypeScript/issues/3628
  • Du point de vue d’un PM TypeScript, je comprends le besoin. La validation de données nécessite souvent des vérifications de type à l’exécution, et il existe de nombreuses bibliothèques pour combler ce manque
    Cela dit, le simple fait qu’il existe beaucoup de bibliothèques avec des choix de conception différents indique que ce n’est pas un problème résolu avec une réponse évidente. On en était conscient dès la conception initiale de TypeScript, et je pense que ce principe a bien tenu.
    À la place, TypeScript est devenu assez puissant pour exprimer précisément, au niveau des types, ce que font réellement les bibliothèques de vérification de types à l’exécution, et les utilisateurs peuvent composer la logique de validation runtime à partir des types via des API. Cela me paraît être un niveau de flexibilité raisonnable

    • Je débute en programmation, et je me demande pourquoi TypeScript ne permet pas de créer des types de données définis par l’utilisateur plus sophistiqués
      Par exemple, j’imagine un langage où l’on pourrait définir comme type un nombre compris dans une certaine plage ou une chaîne correspondant à un motif de code postal, et où le compilateur vérifierait la validité à l’aide de fonctions de validation écrites comme des fonctions ordinaires. Je me demande si l’absence de ce genre de fonctionnalité est un choix de conception visant à éviter une complexité inutile, ou si elle vient de contraintes techniques comme les performances
    • Un plugin de préprocesseur officiel dans le compilateur TypeScript pourrait aider
      Aujourd’hui, ceux qui en ont besoin peuvent déjà écrire un préprocesseur qui génère des objets runtime à partir des informations de type avant de les passer à tsc, mais les efforts sont dispersés. Un préprocesseur enfichable officiel et un écosystème autour pourraient permettre de trouver de bonnes solutions
    • Si Microsoft hébergeait MacroScript sous forme de plugin TypeScript ou de wrapper de haut niveau, cela pourrait résoudre ce problème
      Il suffirait d’allumer l’étincelle pour que la communauté aide à la maintenance, et cela couvrirait divers besoins de génération de code, de la génération de clients aux assertions de type à l’exécution
    • Je me demande ce qu’il pense d’une autre proposition consistant à conserver les informations de type après compilation dans les objets Class
  • Il y a une raison pour ne pas faire ça. TypeScript deviendrait une sorte de runtime au-dessus de JavaScript, et se transformerait en nouveau langage compilé vers JS
    Aujourd’hui, TypeScript est plutôt proche de JavaScript avec des annotations de type. Il existe déjà beaucoup de langages compilés vers JS, donc on peut en utiliser un parmi eux. Les personnes qui veulent un TypeScript avec des types à l’exécution semblent vouloir écrire du code davantage dans le style Java/OOP que JavaScript, mais JavaScript est un langage à typage dynamique, et c’est aussi l’un de ses avantages

    • Pas forcément. Si une macro comme generateTypeInfo!() s’étend en un objet JS encodant le type Foo, cela se compile toujours en JavaScript lisible
      Mais cette macro brise la propriété selon laquelle « TS = JS avec annotations de type, et compiler consiste seulement à supprimer les annotations ». Il faut en effet calculer réellement le type structurel de Foo.
      De plus, comme les types TypeScript sont structurels, inspecter la structure d’un objet à l’exécution permet de connaître son type dans une certaine mesure, mais dès qu’on exige des informations nominales effacées, comme l’appartenance à une union de chaînes, ou un nom de type, cela devient vite un problème difficile à cause de la complétude de Turing du système de types TypeScript et des transformations structurelles implicites
    • On peut avoir les deux. On peut imaginer un monde où le code et les données sont proches, comme avec l’homoiconicité de Lisp
      Plus sérieusement, même sans runtime, exposer les types comme des données permet d’obtenir de la réflexion. Quand on voit le nombre de développeurs qui s’efforcent de l’imiter, ne pas le faire paraît plutôt absurde
    • Je ne comprends pas bien cette logique. TypeScript est déjà un nouveau langage sur-ensemble compilé vers JS
      Les types à l’exécution résolvent le problème consistant à maintenir séparément et en double un système de types pour la compilation et un système de types pour la validation des données, et cela ne semble pas directement lié à l’OOP. Même io-ts, la bibliothèque souvent préférée pour contourner ce problème, s’appuie fortement sur la programmation fonctionnelle
    • Dans des projets suffisamment grands, les langages à typage dynamique ne sont pas une bonne chose, et ressemblent plutôt à un désastre enchevêtré où l’on trébuche sans cesse sur des valeurs inattendues comme NaN
      Dans les workflows que j’ai vus, TypeScript est déjà un langage compilé vers JS ; autant exploiter cet avantage au maximum
    • TypeScript n’est pas simplement du JavaScript avec des annotations de type. Cela peut être l’un de ses modes, mais il sait aussi produire de vieilles structures JS, ce qui peut donner un résultat complètement différent du code TS d’origine
      C’est plus proche de « annotations de type + Babel », et y ajouter une fonctionnalité très utile serait une bonne idée. Le fait de ne pas pouvoir transformer en toute sécurité une chaîne passée à JSON.parse en structure typée est étrange, alors que la plupart des autres langages le permettent
  • Avant ce qui suit, c’est un sujet valide qui mérite clairement discussion, et je ne pense pas qu’il existe objectivement une seule bonne réponse
    TypeScript est une couche optionnelle au-dessus de JavaScript, à l’exception de Enum qui émet un objet, et du code TS devient du JS si l’on efface seulement les types. Tant qu’on ne s’écarte pas beaucoup de ce principe, le vrai besoin ressemble plutôt à une bibliothèque qui génère des sérialiseurs/validateurs à partir des types. Il en existe déjà beaucoup, et cela ressemble finalement à une demande visant à faire de l’une d’elles l’option standard officielle.
    Personnellement, je ne veux pas voir de réflexion à l’exécution entrer dans le langage cœur de TypeScript. J’attache de la valeur au fait qu’à l’exécution il n’y ait que du JavaScript, et que le JS produit reste lisible et débogable même sans sourcemaps

    • En dehors de Enum, il existe d’autres exceptions qui émettent du code à l’exécution, et la plupart sont également considérées comme des erreurs. Les exemples typiques étaient module et namespace, qui étaient des structures à l’exécution
      Cela dit, ces dernières années, ils étaient surtout utilisés dans TypeScript lui-même, et TypeScript s’en est récemment éloigné. Il existe aussi une exception assez populaire : les propriétés de paramètres, une syntaxe qui définit des membres de classe typés depuis les paramètres du constructeur, et qui semble avoir échappé à trop de controverses parce qu’elle réduit le boilerplate redondant
    • Dire que « le code TS devient du JS sans transformation » est possible si l’on voit ainsi l’ensemble du langage et la notation des types
  • Plutôt que de supplier les dieux de TypeScript, je me dis qu’il vaudrait mieux négocier côté JavaScript pour intégrer la vérification de types dans JS
    Si l’on a besoin de types à l’exécution, on utilise des type guards ; si l’on en a souvent besoin, on utilise io-ts ou zod pour écrire les types sous forme de validateurs/codecs/schémas. Tant qu’il n’existe pas de méthode consensuelle en JavaScript, je ne pense pas que la spec TS, le vérificateur de types et la communauté doivent prendre en charge la validation à l’exécution

    • Même si ce n’est pas parfait, je suis assez satisfait de l’approche consistant à utiliser des schémas et des validateurs avec zod, puis à en dériver les types. J’aime le fait que la validation évolue avec les changements de types
      Certains estiment que ce genre de fonctionnalité devrait entrer dans le langage, mais comme chaque bibliothèque comporte de nombreux choix de conception discutables, il peut être préférable de choisir une implémentation adaptée aux besoins du projet.
      Cela dit, des patterns comme la validation à l’exécution de zod et la dérivation de types à partir de schémas ne s’accordent pas toujours bien avec le pattern matching de ts-pattern. Les définitions de types de ces bibliothèques sont labyrinthiques, et il arrive que du code qui semble devoir marcher ne se comporte pas comme prévu. Ce serait vraiment bien si la sûreté à l’exécution et le pattern matching avec vérification d’exhaustivité pouvaient se combiner harmonieusement
    • J’ai de gros doutes sur le fait qu’il soit réellement souhaitable d’ajouter une telle fonctionnalité à JavaScript
      Promise, les décorateurs et le nouvel opérateur pipe me donnent tous l’impression que leur implémentation est partie dans la mauvaise direction
  • Je me souviens qu’un des développeurs de TypeScript a dit que, s’ils recommençaient, ils n’auraient pas ajouté enum, parce que c’est la seule fonctionnalité qui émet du code à l’exécution
    TypeScript ne modifie pas le comportement à l’exécution, et il n’y a rien de spécial propre à TypeScript comme un {#if}

    • C’est vrai. Je ne sais pas si c’était la seule raison, mais c’en était clairement une
      Cela dit, TypeScript possède quelques fonctionnalités qui vont au-delà de « JavaScript + annotations de type » : namespace, la syntaxe qui définit des propriétés de classe depuis les arguments du constructeur, les anciens décorateurs expérimentaux, ou encore la syntaxe du paramètre this, qui disparaît à la compilation mais ressemble à plus qu’une simple annotation de type sur une fonction JS
  • Un meilleur titre serait plutôt « TypeScript, fournissez de la réflexion/des types à l’exécution »
    La meilleure solution actuelle semble probablement être emitDecoratorMetadata. https://www.typescriptlang.org/tsconfig#emitDecoratorMetadata

  • L’auteur semble avoir mal compris les objectifs de conception de TypeScript. Le but n’est pas de produire une sortie JS propre et sans complexité, mais de faire en sorte que la sémantique à l’exécution de TypeScript reste identique à celle de JavaScript.
    À l’exception regrettable de enum, TypeScript devient du JavaScript par simple suppression des annotations de type. L’écosystème autour de TypeScript repose sur l’effacement complet des types, et si cette demande était acceptée, la prise en charge de TS par ESBuild, Deno et Bun pourrait devenir quasiment impossible. Chacun devrait en effet réimplémenter l’intégralité de tsc dans son propre langage.
    À l’inverse, les bibliothèques complexes dont se plaint l’OP sont implémentées dans l’espace utilisateur, elles restent donc compatibles avec ces outils.

    • Il existe de nombreuses façons d’émettre statiquement des informations de type à l’exécution sans runtime. On pourrait convertir keyof en liste de clés de classe, ou faire en sorte que les classes TS initialisent les clés à undefined comme les classes ES6.
      Aujourd’hui, les classes TypeScript suppriment toutes les clés qui ne sont pas explicitement définies, si bien qu’appeler Object.keys() sur une nouvelle instance ne renvoie rien. Le simple fait de transformer les classes TS comme des classes ES6 via une option de tsconfig.json, ou de permettre aux classes ES6 de coexister dans TS, rendrait la génération de code bien plus facile.
      De même, ce serait excellent si une syntaxe statique comme tstypeof Foo::bar était compilée en "string". Même un RTTI de base pourrait faire disparaître immédiatement beaucoup de code TypeScript boilerplate et brouillon.
  • Cet article fait plaisir à voir. J’ai commencé TypeScript en 2018 et, au bout d’environ deux ans, j’ai commencé à croire que ce n’était pas, par nature, une solution complète.
    Je venais de Haskell/C#/F#, et même avec un système de types puissant, TS n’apporte pas beaucoup des avantages de ces langages, sauf en partie pendant le développement. Quand il s’agit de gérer le monde réel, il est en pratique bien plus limité que C#. Si l’on oublie qu’après compilation on retombe sur du JS sans vérification, il faut garder constamment à l’esprit cette abstraction qui fuit pour éviter des bugs qui devraient être impossibles dans un outil se présentant comme statiquement typé.

    • L’objectif de TypeScript a toujours été de s’exécuter sur le Web, pas de fournir un avantage linguistique sur C#.
      Si l’on doit développer en .NET, on utilise C# ; si l’on doit développer pour le Web, on utilise TypeScript. C# a un typage nominal et des génériques réifiés plutôt corrects ; TypeScript a un typage structurel et, en renonçant à la sûreté, dispose d’un système de types puissant pour exprimer des relations de types complexes.
      Je doute qu’un langage qui combinerait les deux soit une bonne idée ; les deux langages fonctionnent bien dans des directions différentes, mais ces directions ne s’accordent pas vraiment.
  • Pour la validation de données à l’exécution, j’ai utilisé https://zod.dev et j’en ai été pleinement satisfait.
    Le fait de pouvoir exprimer son intention directement sur place avec une API fluide, sans définir séparément des types nominaux distincts, était plutôt agréable.

    • Les types nominaux ont un autre usage.
      Exemple simple : dans un domaine, ils sont nécessaires pour distinguer le nombre 2 du montant monétaire 2.