Modul ușor de a obține interfețe TypeScript din cod C #, Java sau Python în orice IDE

Cine nu a experimentat niciodată situația în care trebuie să remediați o eroare și la final aflați că eroarea de pe server a fost un câmp lipsă provenind dintr-o cerere HTTP? Sau o eroare la client, în care codul dvs. Javascript încerca să acceseze un câmp care nu există pe datele care au venit într-un răspuns HTTP de la server? De multe ori, aceste probleme sunt cauzate doar de un nume diferit pentru acest câmp între codul de pe client și server.

Problema

Toți cei care lucrează atât pe back-end-ul cât și pe front-end-ul unei aplicații web trebuie să interogeze și să proceseze date de pe partea serverului și apoi să returneze aceste date pentru a fi consumate de partea client a aplicației. Indiferent de câte straturi este împărțită arhitectura dvs., veți avea întotdeauna marginea dintre server și client, unde solicitările și răspunsurile HTTP transportă datele dintre cele două părți în ambele direcții.

Și nu este vorba doar de erori cu nume diferite - nimeni nu își poate aminti întreaga structură de date a tuturor entităților aplicației. Când scrieți cod, este obișnuit să tastați a .(sau -> or[“). Dacă nu scrieți un nume greșit acolo, vă opriți și vă întrebați „Care naiba a fost numele câmpului respectiv?”. După ce petreci ceva timp încercând să-ți amintești, renunți și alegi calea cea mai plictisitoare. Luați mouse-ul și începeți să căutați fișierul unde definiți toate acele câmpuri la care trebuie să accesați.

Partea plictisitoare a scrierii codului este atunci când nu vă puteți da seama singuri care este codul corect pe care trebuie să îl scrieți.

Uneori nu strică doar să-l googlezi și găsești un răspuns Stack Overflow cu codul acolo, gata să fie copiat. Dar când trebuie să căutați acest răspuns în cadrul proiectului dvs., un proiect mare, în care codul care definește structura de date la care trebuie să accesați se află într-un fișier care nu a fost scris de dvs. ... timpul petrecut pe această cale poate să fie unul sau două ordine de mărime mai mari decât timpul petrecut doar scriind numele potrivit.

TypeScript pentru salvare

Când obișnuiam să scriem Javascript vechi simplu, nu aveam opțiunea de a evita această cale plictisitoare în aceste situații. Dar apoi, la sfârșitul anului 2012, Anders Hejlsberg (tatăl limbajului C #) și echipa sa au creat TypeScript. Misiunea lor a fost de a facilita crearea unor proiecte Javascript de mari dimensiuni.

Partea amuzantă este că, deși acest nou limbaj a fost un superset al Javascriptului, obiectivul său a fost să vă permită să faceți doar un subset de lucruri pe care le făceați cu Javascript. A adăugat noi funcții precum clase, enumere, interfețe, tipuri de parametri și tipuri de returnare.

Dar a eliminat, de asemenea , posibilități , chiar și lucruri care nu erau prea rele, cum ar fi trecerea unui număr ca parametru document.getElementById()și utilizarea *operatorului cu un număr și un șir numeric ca operanzi. Nu mai puteți conta cu conversii de tip implicite, trebuie să fiți explicit și să utilizați .toString()sau parseInt(str)când doriți o conversie de tip. Dar cel mai bun lucru pe care nu îl mai puteți face este să accesați un câmp care nu există într-un obiect.

Deci, atunci când o problemă este rezolvată, una nouă își ia adesea locul. Și aici noua problemă a fost duplicarea codului. Oamenii au început să înlocuiască principiul DRY (Don't Repeat Yourself) cu principiul WET (Write Everything Twice).

Este o bună practică să folosiți diferite clase în diferite straturi, în scopuri diferite, dar nu este cazul aici. Dacă aveți trei straturi (A -> B -> C), nu ar trebui să aveți structuri de date specifice pentru fiecare strat (unul pentru A, unul pentru B și unul pentru C), ci mai degrabă pentru fiecare margine dintre acele straturi ( una între A și B și alta între B și C). Aici, cu excepția cazului în care back-end-ul dvs. este o aplicație Node.js, trebuie să duplicăm aceste declarații de structură a datelor, deoarece suntem în limita dintre două limbaje de programare diferite.

Pentru a evita să scriem totul de două ori, rămânem cu o singură opțiune ...

Generarea codului

Într-o zi lucram la un proiect .NET cu Entity Framework. Avea o diagramă model într-un fișier .edmx și, dacă am schimbat acest fișier, a trebuit să selectez o opțiune pentru a genera clasele pentru entitățile POCO (Plain Old CLR Objects).

Această generare de cod a fost realizată de T4, un motor de șabloane al Visual Studio care a funcționat cu un fișier .tt ca șablon pentru o clasă C #. A rulat codul care citește fișierul model .edmx și scoate clasele în fișiere .cs. După ce mi-am amintit asta, m-am gândit că ar putea fi o soluție pentru a genera interfețe TypeScript și am început să încerc să o fac să funcționeze.

În primul rând, am încercat să îmi scriu propriul șablon. Când am lucrat cu acest lucru și cu Entity Framework, nu a trebuit să schimb niciodată șablonul .tt. Apoi am aflat că Visual Studio nu suporta evidențierea sintaxei în fișierele .tt - a fost ca programarea în notepad, dar mai rău.

Pe lângă faptul că am codul C # al logicii generației, am amestecat și codul TypeScript care trebuia generat, așa. Am instalat o extensie Visual Studio pentru a obține suport pentru sintaxă, dar extensia a definit culorile sintaxei numai pentru tema deschisă a Visual Studio și o folosesc pe cea întunecată. Culorile sintaxei temei deschise pe tema întunecată nu erau citibile, așa că a trebuit să-mi schimb și tema Visual Studio.

Acum, cu evidențierea sintaxei, totul a fost bine. Era timpul să începem să scriem un cod. Am căutat pe google un exemplu de lucru. Ideea mea a fost să o schimb pentru nevoile mele după ce am început să funcționeze, dar ... NU A FUNCȚIONAT!

System.IO.FileNotFoundException: Could not load file or assembly 'System.Runtime, Version=4.2.1.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' or one of its dependencies. The system cannot find the file specified.

Am încercat o mulțime de exemple „de lucru” găsite căutând pe google, dar niciunul nu a funcționat. M-am gândit că poate problema nu este cu Visual Studio sau cu T4 Engine - poate că problema am fost eu, folosind-o greșit.

Apoi, google m-a luat pe această problemă în depozitul .NET Core și am constatat că nu funcționează cu proiectele ASP.NET Core. Dar această eroare a fost o eroare obișnuită în lumea .NET, așa că m-am gândit că aș putea încerca să o soluționez. Am căutat versiunea 4.2.1.0 a System.Runtime.dll, am găsit-o și am încercat să o pun în câteva directoare diferite pentru a vedea dacă Visual Studio o poate găsi ... dar nimic nu a funcționat.

În cele din urmă, am folosit Process Explorer pentru a vedea ce versiune de System.Runtime Visual Studio s-a încărcat și era versiunea 4.0.0.0. Am încercat să folosesc a bindingRedirectpentru a-l forța să folosească aceeași versiune (așa cum am descris aici) și a funcționat! Nu mi-a venit să cred că nu va mai trebui să duplic și să sincronizez manual structurile mele de date între server și client.

Am început să mă gândesc mai mult la asta și un alt gând mă deranja ...

A meritat?

Lucrez pentru o companie petrolieră mare, cu o mulțime de aplicații vechi. Un prieten a trebuit să lucreze cu o mașină virtuală, deoarece aplicația pe care o depana uneori funcționa doar în Windows XP. O altă aplicație pe care a trebuit să o lucrez într-o zi a funcționat doar cu Visual Studio 2010. O altă aplicație care folosea Code Contracts a funcționat doar cu Visual Studio 2013, deoarece extensia Code Contracts nu a funcționat în Visual Studio 2015 sau 2017.

Din 2012, când am început să lucrez acolo până la începutul anului 2019, nu am avut niciodată șansa să dezvolt o nouă aplicație. Toată munca mea a fost întotdeauna cu mizeriile altor dezvoltatori. Anul trecut am început să studiez mai multe despre arhitectura software și am citit cartea „Arhitectura curată” a unchiului Bob.

Acum, că am început acest nou an cu această oportunitate, pentru prima dată în această companie creez o aplicație web de la zero și vreau să fac o treabă bună. Aleg ASP.NET Core pentru back-end-ul meu, React pentru front-end și va fi una dintre primele aplicații din această companie care vor rula într-un container Docker în noul nostru cluster Kubernetes.

Un alt dezvoltator sărac va trebui să lucreze la acest proiect în viitor, cu codul meu și toată mizeria mea, și nu vreau să se ocupe de codul rău. Vreau ca toți dezvoltatorii de după mine să vrea să lucreze la acest proiect. Acest lucru nu se va întâmpla dacă trebuie să piardă o zi de muncă doar pentru a obține generarea de cod client din structurile de date back-end. M-ar urî apoi (iar unii dintre ei m-ar urî deja pentru că am introdus codul TypeScript într-un proiect când TypeScript era încă în versiunea 0.9).

Când scriem un cod care nu este al nostru, avem responsabilitatea de a facilita altor oameni să lucreze la el.

După ce m-am gândit la asta, am ajuns la o concluzie:

Ar trebui să evităm dependențele de orice nu poate fi gestionat de managerul de pachete al tehnologiei la alegere.

În acest caz, în afară de dependențele de Visual Studio și Windows, aș face proiectul să depindă de o remediere a erorilor care ar trebui reparată de Microsoft (și se pare că nu are nicio prioritate). Deci, cel mai bine este să copiați acest cod și să-l sincronizați manual, decât să puneți o dependență pe acest motor T4.

Aleg să folosesc .NET Core, dar dacă vreun dezvoltator în viitor dorește să lucreze la acest proiect folosind Linux, nu le pot opri.

Soluția finală (TL; DR)

Duplicate code is bad, but dependency on third party tools is worse. So, what can we do to avoid duplication of data structures and not depend on any specific IDE / plugin / extension / tool for development?

It took me some time to realize that the only tool that I needed was there all this time, inside the language runtime: Reflection.

I realized I could write some code that runs on the startup of my back-end ASP.NET Core app only in development mode. This code could use reflection to read the metadata about names and types of all the data structures that I wanted to generate TypeScript interfaces. I just needed to map C# primitives to TypeScript primitives, write the .d.ts TypeScript definitions in a specific folder, and I’d be done.

Every time I changed some data structure in the back-end, it would override the interfaces definitions inside a .d.ts files when I ran the code to test it. When I got to the part of writing the client code to use the data structure that changed, the interfaces would already be updated.

This approach can be used by projects in .NET, Java, Python, and any other language that has support for code reflection, without adding a dependency on any IDE / plugin / extension / tool.

I wrote a simple example using C# with ASP.NET Core and published it on GitHub here. It just takes from all classes that inherit Microsoft.AspNetCore.Mvc.ControllerBase and all types from parameters and returns types of public methods that have HttpGet or HttpPost attributes.

Here is what the generated interfaces look like:

You can generate other types of code too

I used it to generate interfaces and enums for data structures only, but think about the code below:

It’s much less of a pain to keep this code in sync with all the possible MVC controllers and actions than it was to keep the data structures in sync. But do I need to write this code by hand? Couldn’t it be generated too?

I can’t generate C# interfaces from C# concrete implementations, because I need the code to compile and run before I can use reflection to generate it. But with client code that needs to be kept in sync with server code, I can generate it. This way of code generation can be used beyond the data structure interfaces.

If you don’t like TypeScript…

It doesn’t need to be written with TypeScript. If you don’t like TypeScript and prefer to use plain Javascript, you can write your .js files and use TypeScript just as a tool (if you use Visual Studio Code you are already using it). That way, you can generate helper functions that convert your data structures to the same structures. It seems weird, but it would help the TypeScript Language Service to analyse your code and tell Visual Studio Code with fields that exist in each object, so it could help you to write your code.

Conclusion

We, as developers, have a responsibility to other developers that will have to work on our code. Don’t leave a mess for them to clean up, because they won’t (or at least they won’t want to!). They will likely only make it worse for the next one.

You should avoid at all costs any development and runtime dependencies that cannot be handled by the package manager. Don’t make your project the one that others developers will hate working on.

Thanks for reading!

PS 1: This repository with my code is just an example. The code that converts C# classes into TypeScript interfaces there is not good. You can do a lot better, and maybe we already have some NuGet package that do this.

PS 2: I love TypeScript. If you love TypeScript too, you may want to take a look at these links, from before it was announced by Microsoft in 2012:

  • What’s Microsoft’s father of C#’s next trick? Microsoft Technical Fellow Anders Hejlsberg is working on something to do with JavaScript tools. Here are a few clues about his latest project.
  • A HackerNews discussion: “Anders Hejlsberg Is Right: You Cannot Maintain Large Programs In JavaScript”
  • A Channel9 video: “Anders Hejlsberg: Introducing TypeScript”