La forma fácil de obtener interfaces TypeScript desde código C #, Java o Python en cualquier IDE

¿Quién nunca ha experimentado la situación en la que tiene que corregir un error y al final descubre que el error en el servidor era un campo faltante proveniente de una solicitud HTTP? ¿O un error en el cliente, donde su código Javascript intentaba acceder a un campo que no existe en los datos que llegaron en una respuesta HTTP del servidor? Muchas veces, estos problemas son causados ​​simplemente por un nombre diferente para este campo entre el código del cliente y el servidor.

El problema

Todos los que trabajan tanto en el back-end como en el front-end de una aplicación web deben consultar y procesar datos en el lado del servidor y luego devolver estos datos para que sean consumidos por el lado del cliente de la aplicación. No importa en cuántas capas esté dividida su arquitectura, siempre tendrá la ventaja entre el servidor y el cliente, donde las solicitudes y respuestas HTTP llevan los datos entre esos dos lados en ambas direcciones.

Y esto no se trata solo de los errores con diferentes nombres, nadie puede recordar la estructura de datos completa de todas las entidades de la aplicación. Cuando escribe código, es común escribir a .(o -> or[“). Si no escribe un nombre incorrecto allí, deténgase y pregúntese: "¿Cuál diablos era el nombre de ese campo?". Después de pasar algún tiempo tratando de recordar, te rindes y eliges el camino más aburrido. Coges el ratón y empiezas a buscar el archivo donde defines todos aquellos campos a los que necesitas acceder.

La parte aburrida de escribir código es cuando no puede averiguar por sí mismo cuál es el código correcto que necesita escribir.

A veces no está de más buscarlo en Google y encontrar una respuesta de Stack Overflow con el código allí, listo para ser copiado. Pero cuando tiene que buscar esta respuesta dentro de su proyecto, un proyecto grande, donde el código que define la estructura de datos a la que tiene que acceder está en un archivo que no fue escrito por usted ... el tiempo que pasa en esta ruta puede ser uno o dos órdenes de magnitud mayor que el tiempo dedicado a escribir el nombre correcto.

TypeScript al rescate

Cuando solíamos escribir simplemente Javascript antiguo, no teníamos una opción para evitar este aburrido camino en estas situaciones. Pero luego, a fines de 2012, Anders Hejlsberg (el padre del lenguaje C #) y su equipo crearon TypeScript. Su misión era facilitar la creación de grandes proyectos de Javascript a escala.

La parte divertida es que, si bien este nuevo lenguaje era un superconjunto de Javascript, su objetivo era permitirle hacer solo un subconjunto de cosas que solía hacer con Javascript. Se añadió nuevas características como las clases, enumeraciones, interfaces, tipos de parámetros y tipos de devolución.

Pero también eliminó posibilidades , incluso cosas que no estaban tan mal, como pasar un número como parámetro document.getElementById()y usar el *operador con un número y una cadena numérica como operandos. Ya no puede contar con conversiones de tipo implícitas, debe ser explícito y usar .toString()o parseInt(str)cuando desee una conversión de tipo. Pero lo mejor que ya no puede hacer es acceder a un campo que no existe en un objeto.

Entonces, cuando se resuelve un problema, a menudo uno nuevo ocupa su lugar. Y aquí el nuevo problema fue la duplicación de código. La gente comenzó a reemplazar el principio DRY (Don't Repeat Yourself) por el principio WET (Escribe todo dos veces).

Es una buena práctica usar diferentes clases en diferentes capas, para diferentes propósitos, pero no es el caso aquí. Si tiene tres capas (A -> B -> C), no debería tener estructuras de datos específicas para cada capa (una para A, una para B y otra para C), sino para cada borde entre esas capas ( uno entre A y B y otro entre B y C). Aquí, a menos que su back-end sea una aplicación Node.js, tenemos que duplicar estas declaraciones de estructura de datos porque estamos en el límite entre dos lenguajes de programación diferentes.

Para evitar escribir todo dos veces, solo nos queda una opción ...

Codigo de GENERACION

Un día estaba trabajando en un proyecto .NET con Entity Framework. Tenía un diagrama de modelo en un archivo .edmx, y si cambiaba este archivo, tenía que seleccionar una opción para generar las clases para las entidades POCO (Objetos CLR antiguos simples).

Esta generación de código fue realizada por T4, un motor de plantillas de Visual Studio que trabajaba con un archivo .tt como plantilla para una clase C #. Ejecutó el código que lee el archivo de modelo .edmx y genera las clases en archivos .cs. Después de recordar eso, pensé que podría ser una solución para generar interfaces TypeScript y comencé a intentar que funcionara.

Primero, intenté escribir mi propia plantilla. Cuando trabajé con esto y Entity Framework, nunca tuve que cambiar la plantilla .tt. Luego descubrí que Visual Studio no admitía el resaltado de sintaxis en archivos .tt; era como programar en el bloc de notas, pero peor.

Además de tener el código C # de la lógica de generación, también había mezclado con él el código TypeScript que tenía que generar, así. Instalé una extensión de Visual Studio para obtener soporte de sintaxis, pero la extensión definió colores de sintaxis solo para el tema claro de Visual Studio, y uso el oscuro. Los colores de la sintaxis del tema claro en el tema oscuro eran ilegibles, así que también tuve que cambiar mi tema de Visual Studio.

Ahora, con el resaltado de sintaxis, todo estaba bien. Era el momento de empezar a escribir código. Busqué en Google un ejemplo funcional. Mi idea era cambiarlo para mis necesidades después de que lo hiciera funcionar, pero… ¡NO FUNCIONÓ!

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.

Probé muchos ejemplos "funcionales" que se encuentran buscando en Google, pero ninguno funcionó. Pensé que tal vez el problema no era con Visual Studio o con el motor T4, tal vez el problema era yo, usándolo mal.

Luego, Google me habló de este problema en el repositorio de .NET Core y descubrí que no funcionaba con los proyectos de ASP.NET Core. Pero este error era un error común en el mundo .NET, así que pensé que podría intentar solucionarlo. Busqué esa versión 4.2.1.0 de System.Runtime.dll, la encontré y traté de ponerla en algunos directorios diferentes para ver si Visual Studio podía encontrarla… pero nada funcionó.

Finalmente, usé Process Explorer para ver qué versión de System.Runtime Visual Studio se había cargado, y era la versión 4.0.0.0. Intenté usar un bindingRedirectpara forzarlo a usar la misma versión (como describí aquí), ¡y funcionó! No podía creer que ya no tendría que duplicar y sincronizar manualmente mis estructuras de datos entre el servidor y el cliente.

Empecé a pensar más en eso, y otro pensamiento me estaba molestando ...

¿Valió la pena?

Trabajo para una gran empresa petrolera, con muchas aplicaciones heredadas. Un amigo tuvo que trabajar con una máquina virtual porque la aplicación que estaba depurando a veces solo funcionaba en Windows XP. Otra aplicación en la que tuve que trabajar un día solo funcionó con Visual Studio 2010. Otra que usó Code Contracts solo funcionó con Visual Studio 2013 porque la extensión Code Contracts no funcionó en Visual Studio 2015 o 2017.

Desde 2012 cuando comencé a trabajar allí hasta principios de 2019, nunca tuve la oportunidad de desarrollar una nueva aplicación. Todo mi trabajo siempre fue con los líos de otros desarrolladores. El año pasado comencé a estudiar más sobre arquitectura de software y leí el libro “Arquitectura limpia” del tío Bob.

Ahora que comencé este nuevo año con esta oportunidad, por primera vez en esta empresa estoy creando una aplicación web desde cero y quiero hacer un buen trabajo. Elijo ASP.NET Core para mi back-end, React para el front-end, y será una de las primeras aplicaciones de esta empresa que se ejecutará en un contenedor Docker en nuestro nuevo clúster de Kubernetes.

Otro desarrollador pobre tendrá que trabajar en este proyecto en el futuro, con mi código y todo mi lío, y no quiero que tengan que lidiar con un código incorrecto. Quiero que todos los desarrolladores después de mí quieran trabajar en este proyecto. Esto no sucederá si tienen que perder un día de trabajo solo para que funcione la generación de código de cliente a partir de estructuras de datos de back-end. Entonces me odiarían (y algunos de ellos ya me odiarían por poner código TypeScript en un proyecto cuando TypeScript todavía estaba en la versión 0.9).

Cuando escribimos un código que no es nuestro, tenemos la responsabilidad de facilitar que otras personas trabajen en él.

Después de pensar en eso, llegué a una conclusión:

Debemos evitar dependencias de cualquier cosa que no pueda ser manejada por el administrador de paquetes de la tecnología elegida.

En este caso, además de las dependencias de Visual Studio y Windows, haría que el proyecto dependiera de una corrección de errores que Microsoft debería corregir (y parece que no tiene ninguna prioridad). Por lo tanto, es mejor duplicar este código y sincronizarlo manualmente que poner una dependencia en este motor T4.

Elijo usar .NET Core, pero si algún desarrollador en el futuro quiere trabajar en este proyecto usando Linux, no puedo detenerlo.

La solución final (TL; DR)

El código duplicado es malo, pero la dependencia de herramientas de terceros es peor. Entonces, ¿qué podemos hacer para evitar la duplicación de estructuras de datos y no depender de ningún IDE / complemento / extensión / herramienta específica para el desarrollo?

Me tomó un tiempo darme cuenta de que la única herramienta que necesitaba estaba ahí todo este tiempo, dentro del tiempo de ejecución del lenguaje: Reflection .

Me di cuenta de que podía escribir un código que se ejecuta en el inicio de mi aplicación de back-end ASP.NET Core solo en modo de desarrollo. Este código podría usar la reflexión para leer los metadatos sobre los nombres y tipos de todas las estructuras de datos que quería generar interfaces de TypeScript. Solo necesitaba mapear primitivas de C # a primitivas de TypeScript, escribir las definiciones de TypeScript .d.ts en una carpeta específica, y estaría listo.

Cada vez que cambiaba alguna estructura de datos en el back-end, anulaba las definiciones de interfaces dentro de un archivo .d.ts cuando ejecutaba el código para probarlo. Cuando llegué a la parte de escribir el código del cliente para usar la estructura de datos que cambió, las interfaces ya estaban actualizadas.

Este enfoque puede ser utilizado por proyectos en .NET, Java, Python y cualquier otro lenguaje que tenga soporte para la reflexión de código, sin agregar una dependencia en ningún IDE / complemento / extensión / herramienta.

Escribí un ejemplo simple usando C # con ASP.NET Core y lo publiqué en GitHub aquí. Solo se necesita de todas las clases que heredan Microsoft.AspNetCore.Mvc.ControllerBasey todo tipo de parámetros y tipos de devoluciones métodos públicos que tienen HttpGeto HttpPostatributos.

Así es como se ven las interfaces generadas:

También puede generar otros tipos de código

Lo usé para generar interfaces y enumeraciones solo para estructuras de datos, pero piense en el código a continuación:

Mantener este código sincronizado con todos los controladores y acciones posibles de MVC es mucho menos complicado que mantener sincronizadas las estructuras de datos. Pero, ¿necesito escribir este código a mano? ¿No se podría generar también?

No puedo generar interfaces C # a partir de implementaciones concretas de C #, porque necesito que el código se compile y se ejecute antes de poder usar la reflexión para generarlo. Pero con el código del cliente que debe mantenerse sincronizado con el código del servidor, puedo generarlo. Esta forma de generación de código se puede utilizar más allá de las interfaces de estructura de datos.

Si no le gusta 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”