Portar APIs a TypeScript como un solucionador de problemas

La interfaz React del programa Ejecutar se ha convertido de JavaScript a TypeScript. Pero el backend, escrito en Ruby, no se tocó. Sin embargo, los problemas asociados con este backend hicieron que los desarrolladores del proyecto pensaran en cambiar de Ruby a TypeScript. La traducción del material que publicamos hoy está dedicada a la historia de portar el backend Ejecutar programa de Ruby a TypeScript, y qué problemas esto ayudó a resolver.



Al usar el backend de Ruby, a veces olvidamos que algunas propiedades de API almacenan una serie de cadenas, no una simple cadena. A veces cambiamos un fragmento de API al que se accedió en diferentes lugares, pero se olvidó de actualizar el código en uno de estos lugares. Estos son los problemas habituales de un lenguaje dinámico que son característicos de cualquier sistema cuyo código no esté 100% cubierto por las pruebas. (Esto, aunque menos común, ocurre cuando el código está completamente cubierto por las pruebas).

Al mismo tiempo, estos problemas han desaparecido de la interfaz desde que lo cambiamos a TypeScript. Tengo más experiencia en la programación del servidor que en el cliente, pero, a pesar de esto, cometí más errores al trabajar con el back-end y no con la interfaz. Todo esto indica que el backend también debe convertirse a TypeScript.

Porté el backend de Ruby a TypeScript en marzo de 2019 en aproximadamente 2 semanas. ¡Y todo funcionó como debería! Implementamos un nuevo código en producción el 14 de abril de 2019. Era una versión beta disponible para un número limitado de usuarios. Después de eso, nada se rompió. Los usuarios ni siquiera notaron nada. Aquí hay un gráfico que ilustra el estado de nuestra base de código antes e inmediatamente después de la transición. El eje x representa el tiempo (en días), el eje y representa el número de líneas de código.


Traducción del front-end de JavaScript a TypeScript, y traducción del backend de Ruby a TypeScript

Durante el proceso de portabilidad, escribí una gran cantidad de código auxiliar. Entonces, tenemos nuestra propia herramienta para ejecutar pruebas con un volumen de 200 líneas. Tenemos una biblioteca de 120 líneas para trabajar con la base de datos, así como una biblioteca de enrutamiento más grande para la API que conecta el código de front-end y back-end.

En nuestra propia infraestructura, lo más interesante para hablar es el enrutador. Es un contenedor para Express, que garantiza la correcta aplicación de los tipos que se utilizan tanto en el código del cliente como del servidor. Esto significa que cuando una parte de la API cambia, la otra ni siquiera se compila sin realizar cambios para eliminar las diferencias.

Aquí hay un controlador de back-end que devuelve una lista de publicaciones de blog. Este es uno de los fragmentos de código similares más simples del sistema:

router.handleGet(api.blog, async () => {
  return {
    posts: blog.posts,
  }
})

Si cambiamos el nombre de la clave postsa blogPosts, obtenemos un error de compilación, cuyo texto se muestra a continuación (aquí, por brevedad, se omite la información sobre los tipos de objetos).

Property 'posts' is missing in type '...' but required in type '...'.

Cada punto final está definido por un objeto de vista api.someNameHere. Este objeto es compartido por el cliente y el servidor. Tenga en cuenta que los tipos no se mencionan directamente en la declaración del controlador. Todos se infieren del argumento api.blog.

Este enfoque funciona para puntos finales simples, como el punto final descrito anteriormente blog. Pero es adecuado para puntos finales más complejos. Por ejemplo, una API de punto final para trabajar con lecciones tiene una clave profundamente anidada de tipo lógico .lesson.steps[index].isInteractive. Gracias a todo esto, ahora es imposible cometer los siguientes errores:

  • Si intentamos acceder isinteractiveal cliente o intentamos devolver dicha clave del servidor, el código no se compilará. El nombre de la clave debería verse isInteractivecon mayúscula I.
  • isInteractive — .
  • isInteractive number, , , .
  • API, , isInteractive — , , , , , , , .

Tenga en cuenta que todo esto incluye la generación de código. Esto se hace usando io-ts y un par de cientos de líneas de código de nuestro propio enrutador.

Declarar tipos de API requiere trabajo adicional, pero el trabajo es simple. Al cambiar la estructura de la API, necesitamos saber cómo cambia la estructura del código. Hacemos cambios en las declaraciones de la API, y luego el compilador nos señala todos los lugares donde el código necesita ser reparado.

Es difícil apreciar la importancia de estos mecanismos hasta que los use por un tiempo. Podemos mover objetos grandes de un lugar en la API a otro, cambiar el nombre de las claves, podemos dividir objetos grandes en partes, fusionar objetos pequeños en un objeto, dividir o fusionar puntos finales completos. Y podemos hacer todo esto sin preocuparnos por el hecho de que olvidamos hacer los cambios apropiados en el código del cliente o servidor.

Aquí hay un ejemplo real. Recientemente, pasé unas 20 horas en cuatro días libres rediseñando el Programa de ejecución de API . Toda la estructura de la API ha cambiado. Al comparar el nuevo código de cliente y servidor con el antiguo, se registraron decenas de miles de cambios de línea. Rediseñé el código de enrutamiento del lado del servidor (como el anteriorhandleGet) Reescribí todas las declaraciones de tipo para la API, haciendo muchos de ellos grandes cambios estructurales. Y, además, reescribí todas las partes del cliente en las que se llamaron las API modificadas. Durante este trabajo, se cambiaron 246 de los 292 archivos de origen.

En la mayoría de este trabajo, me basé solo en un sistema de tipos. En la última hora de este caso de 20 horas, comencé a ejecutar pruebas que, en su mayor parte, terminaron con éxito. Al final, realizamos una serie completa de pruebas y encontramos tres pequeños errores.

Todos estos fueron errores lógicos: condiciones que accidentalmente llevaron al programa al lugar equivocado. Típicamente, un sistema de tipos no ayuda a encontrar tales errores. Tomó varios minutos corregir estos errores. Esta API rediseñada se implementó hace unos meses. Cuando lees algo ennuestro sitio : es esta API la que emite materiales relevantes.

Esto no significa que el sistema de tipo estático garantice que el código siempre será correcto. Este sistema no permite prescindir de las pruebas. Pero simplifica enormemente la refactorización.

Te contaré sobre la generación automática de código. A saber, usamos esquemas para generar definiciones de tipo a partir de la estructura de nuestra base de datos. El sistema se conecta a la base de datos Postgres, analiza los tipos de columna y escribe las definiciones de tipo TypeScript correspondientes en el archivo normal .d.tsutilizado por la aplicación.

Nuestro script de migración mantiene actualizado un archivo con tipos de esquema de base de datos cada vez que se inicia. Debido a esto, no tenemos que admitir manualmente estos tipos. Los modelos usan definiciones de tipos de bases de datos para garantizar que el código de la aplicación acceda correctamente a todo lo almacenado en la base de datos. No faltan tablas, ni columnas faltantes, ni entradas nullen columnas no compatibles null. Recordamos procesar correctamente nullen columnas de soporte null. Y todo esto se comprueba estáticamente en tiempo de compilación.

Todo esto en conjunto crea una cadena confiable de transferencia de información de tipo estático, que se extiende desde la base de datos a las propiedades de los componentes React en la interfaz:

  • , ( API) , .
  • API , API, ( ) .
  • React- , API, .

Mientras trabajaba en este material, no pude recordar un solo caso de inconsistencia en el código asociado con la API que pasó la compilación. No tuvimos fallas de producción que surgieron porque el código del cliente y del servidor relacionado con la API tenía ideas diferentes sobre el formulario de datos. Y todo esto no es el resultado de pruebas automatizadas. Nosotros, para la API en sí, no escribimos pruebas.

Esto nos coloca en una posición extremadamente agradable: podemos concentrarnos en las partes más importantes de la aplicación. Paso muy poco tiempo haciendo conversiones de tipos. Mucho menos de lo que pasé identificando las causas de errores confusos que penetraron capas de código escritas en Ruby o JavaScript, y luego causaron extrañas excepciones en algún lugar muy lejos de la fuente del error.

Así es como se ve el proyecto después de traducir el backend a TypeScript. Como puede ver, se ha escrito mucho código desde la transición. Tuvimos suficiente tiempo para evaluar las consecuencias de la decisión.


TypeScript se utiliza en la interfaz y el backend del proyecto.

Aquí no hemos planteado la pregunta habitual para tales publicaciones, que es lograr los mismos resultados no a través de la mecanografía, sino mediante el uso de pruebas. Tales resultados no pueden lograrse usando solo pruebas. Nosotros, muy posiblemente, hablaremos más sobre esto.

¡Queridos lectores! ¿Tradujo proyectos escritos en otros idiomas a TypeScript?


All Articles