A TypeScript interfészek C #, Java vagy Python kódokból történő egyszerű beszerzése bármely IDE-ben

Ki soha nem tapasztalta azt a helyzetet, amikor hibát kell kijavítania, és a végén megtudja, hogy a szerver hibája egy hiányzó mező volt, amely egy HTTP kérésből származott? Vagy hiba történt az ügyfélnél, ahol a Javascript-kód megpróbált elérni egy olyan mezőt, amely nem létezik a szerverről HTTP-válaszként kapott adatokon? Sokszor ezeket a problémákat csak az okozza, hogy ennek a mezőnek a kliens és a szerver kódja között más néven szerepel.

A probléma

Mindenkinek, aki mind a webalkalmazás hátterén, mind a kezelőfelületén dolgozik, lekérdeznie és feldolgoznia kell az adatokat a kiszolgáló oldalán, majd vissza kell adnia ezeket az adatokat az alkalmazás ügyféloldala számára. Nem számít, hány rétegre van felosztva az architektúrája, mindig megvan az él a szerver és az ügyfél között, ahol a HTTP kérések és válaszok továbbítják az adatokat a két oldal között mindkét irányban.

És ez nem csak a különböző nevű hibákról szól - senki sem emlékszik az alkalmazás összes entitásának teljes adatstruktúrájára. Amikor kódot ír, gyakran beír egy .(vagy -> or[“) szót . Ha nem rossz nevet írsz oda, megállsz, és felteszed magadnak a kérdést: „Mi a fene volt annak a mezőnek a neve?”. Miután egy kis időt töltött az emlékezéssel, feladja, és a legunalmasabb utat választja. Fogja az egeret, és elkezdi keresni a fájlt, ahol meghatározza mindazokat a mezőket, amelyekhez hozzáférnie kell.

A kódírás unalmas része, amikor nem tudja egyedül kitalálni, mi a helyes kód, amelyet írnia kell.

Néha nem árt csak google-ozni, és egy Stack Overflow választ talál az ott található kóddal, készen áll a másolásra. De amikor ezt a választ kell keresned a projektedben, egy nagy projektben, ahol az a kód, amely meghatározza az adatstruktúrát, amelyhez hozzáférned kell, egy fájlban található, amelyet nem te írtál ... legyen egy-két nagyságrenddel nagyobb, mint a megfelelő név megírásával töltött idő.

Írja be a TypeScript-t

Amikor csak egyszerű régi Javascriptet írtunk, akkor nem volt lehetőségünk elkerülni ezt az unalmas utat ezekben a helyzetekben. De aztán 2012 végén Anders Hejlsberg (a C # nyelv atyja) és csapata létrehozta a TypeScript-t. Küldetésük az volt, hogy megkönnyítsék a nagyméretű Javascript projektek létrehozását.

A vicces rész az, hogy bár ez az új nyelv a Javascript szuperhalmaza volt, célja az volt, hogy lehetővé tegye számodra a Javascriptdel korábban használt dolgok egy részhalmazát . Ez a hozzáadott új funkciók , mint osztályok, enums, interfészek, paraméterek típusa és visszatérő típusok.

De megszüntette a lehetőségeket , még olyan dolgokat is, amelyek nem voltak túl rosszak, mint például egy szám paraméterként történő továbbítása document.getElementById(), és az *operátor használata számokkal és numerikus karakterláncokkal operandusként. Az implicit típusú konverziókkal már nem számolhat, explicitnek kell lennie, és használnia kell, .toString()vagy parseInt(str)amikor típus konverziót szeretne. De a legjobb dolog, amit már nem tehet meg, az az, hogy olyan mezőhöz fér hozzá, amely nem létezik egy objektumban.

Tehát, ha egy probléma megoldódik, gyakran egy új lép a helyére. És itt az új probléma a kód másolása volt. Az emberek elkezdték a DRY (Ne ismételd meg önmagad) elvét a WET elvre (Mindent kétszer írj).

Jó gyakorlat, ha különböző osztályokat használunk különböző rétegekben, különböző célokra, de ez itt nem így van. Ha három rétege van (A -> B -> C), akkor nem szabad minden adatréteghez külön adatstruktúrát készíteni (egyet A-hoz, egyet B-hez és egyet C-hez), hanem a rétegek közötti élekhez ( egyik A és B, másik B és C között). Itt, hacsak a háttér nem Node.js alkalmazás, meg kell ismételnünk ezeket az adatstruktúra-deklarációkat, mert két különböző programozási nyelv között vagyunk.

Annak elkerülése érdekében, hogy mindent kétszer írjunk, egyetlen lehetőségünk marad ...

Kódgenerálás

Egy nap egy .NET projekten dolgoztam az Entity Framework-szel. Volt egy modelldiagram egy .edmx fájlban, és ha megváltoztattam ezt a fájlt, ki kellett választanom egy opciót az osztályok előállításához a POCO entitásokhoz (Plain Old CLR Objects).

Ezt a kódgenerálást a T4, a Visual Studio sablonmotorja végezte, amely egy .tt fájllal dolgozott sablonként egy C # osztályhoz. Futtatta a kódot, amely beolvassa az .edmx modell fájlt, és az osztályokat .cs fájlokban adja ki. Miután erre emlékeztem, arra gondoltam, hogy megoldás lehet a TypeScript interfészek előállítására, és elkezdtem megpróbálni működőképessé tenni.

Először megpróbáltam saját sablont írni. Amikor ezzel és az Entity Framework-szel dolgoztam, soha nem kellett megváltoztatnom a .tt sablont. Aztán megtudtam, hogy a Visual Studio nem támogatja a .tt fájlokban a szintaxis kiemelését - olyan volt, mint a jegyzettömbben történő programozás, de még rosszabb.

Amellett, hogy rendelkezem a generációs logika C # kódjával, összekevertem vele a TypeScript kódot is, amelyet így kellett létrehozni. Telepítettem egy Visual Studio kiterjesztést a szintaxis támogatásához, de a kiterjesztés csak a Visual Studio világos témájához definiált szintaxisszíneket, a sötétet pedig használom. A világos téma szintaxisának színei a sötét témában olvashatatlanok voltak, ezért nekem is meg kellett változtatnom a Visual Studio témámat.

A szintaxis kiemelésével minden jó volt. Itt volt az ideje elkezdeni írni néhány kódot. A google-n kerestem működő példát. Az volt az ötletem, hogy megváltoztassam az igényeimhez, miután működésbe léptem, de ... NEM MŰKÖDött!

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.

Sok „működő” példát kipróbáltam, amelyek a google-n találtak, de egyik sem működött. Arra gondoltam, hogy lehet, hogy nem a Visual Studio vagy a T4 motor miatt van a probléma - lehet, hogy én vagyok a probléma, rosszul használtam.

Aztán a Google felkeltette ezt a kérdést a .NET Core adattárban, és megállapítottam, hogy ez nem működik az ASP.NET Core projektekkel. De ez a hiba gyakori hiba volt a .NET világában, ezért arra gondoltam, hogy megpróbálhatok megoldást találni rá. Megkerestem a System.Runtime.dll 4.2.1.0 verzióját, megtaláltam, és megpróbáltam különböző könyvtárakba helyezni, hátha megtalálja a Visual Studio ... de semmi sem működött.

Végül a Process Explorer segítségével láttam, hogy a System.Runtime Visual Studio melyik verzióját töltötte be, és ez a 4.0.0.0 verzió volt. Megpróbáltam a használatával bindingRedirectarra kényszeríteni, hogy ugyanazt a verziót használja (ahogy itt leírtam), és ez működött! Nem hittem el, hogy nem kell többé lemásolnom és manuálisan szinkronizálnom az adatstruktúrákat a szerver és az ügyfél között.

Kezdtem többet foglalkozni vele, és egy másik gondolat zavart ...

Megérte?

Egy nagy olajipari vállalatnál dolgozom, sok régi alkalmazással. Egy barátjának virtuális géppel kellett dolgoznia, mert az általa hibakeresett alkalmazás néha csak Windows XP-ben működött. Egy másik alkalmazás, amelyet egy napon kellett dolgoznom, csak a Visual Studio 2010-mel működött együtt. Egy másik, amely a Code Contracts-ot használta, csak a Visual Studio 2013-mal működött, mert a Code Contracts bővítmény nem működött a Visual Studio 2015-ben vagy 2017-ben.

2012 óta, amikor ott kezdtem dolgozni 2019 elejéig, soha nem volt lehetőségem új alkalmazás kifejlesztésére. Minden munkám mindig más fejlesztők zűrzavarával járt. Tavaly elkezdtem többet tanulmányozni a szoftverarchitektúráról, és elolvastam Bob bácsi „Tiszta építészet” című könyvét.

Most, hogy ezzel a lehetőséggel kezdtem az új évet, ebben a cégben először hozok létre egy webalkalmazást a semmiből, és jó munkát szeretnék végezni. Az hátlapomhoz az ASP.NET Core-ot választom, a kezelőfelülethez pedig a React-et, és ez lesz a vállalat első olyan alkalmazásai közül, amelyek egy Docker-tárolóban futnak az új Kubernetes-fürtünkben.

Egy másik szegény fejlesztőnek a jövőben ezen a projekten kell dolgoznia, az én kódommal és minden rendetlenségemmel, és nem akarom, hogy rossz kóddal kelljen foglalkozniuk. Szeretném, ha minden fejlesztő utánam akarna dolgozni ezen a projekten. Ez nem fog megtörténni, ha egy nap munkáját el kell veszíteniük csak azért, hogy a háttér-adatstruktúrák kliens kódjának generálása működjön. Utálnának (és néhányan már utálnának azért, mert a TypeScript kódot egy projektbe tettem, amikor a TypeScript még a 0.9 verzióban volt).

Ha olyan kódot írunk, amely nem a miénk, akkor felelősségünk, hogy megkönnyítsük más emberek számára a dolgozását.

Miután belegondoltam, arra a következtetésre jutottam:

Kerülnünk kell a függőséget bármitől, amit a választott technológia csomagkezelője nem tud kezelni.

Ebben az esetben a Visual Studio és a Windows függőségei mellett a projektet egy hibajavítástól tenném függővé, amelyet a Microsoftnak javítania kellene (és úgy tűnik, hogy nincs prioritása). Tehát a legjobb, ha lemásoljuk ezt a kódot, és manuálisan szinkronizáljuk, mint hogy függőséget hozzunk ebből a T4 motorból.

A .NET Core használatát választom, de ha a jövőben valamelyik fejlesztő Linuxon akar dolgozni ezen a projekten, akkor nem állíthatom le őket.

A végső megoldás (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”