En una aplicación Typescript, ¿cómo se puede asegurar en tiempo de ejecución que un dato es de un determinado tipo?

En este post revisamos una de las causas más habituales de errores de integración entre front y back y cómo solucionarla.

El problema

En tiempo de compilación, asegurarse de ello resulta trivial: el programa no compilará si no es así.

Se vuelve algo más interesante en tiempo de ejecución, ya que el código se ha transpilado a Javascript y el concepto de tipo no existe, más allá de los tipos básicos del lenguaje.

En el lado front, esta situación se da sobre todo con la información que llega desde fuera de nuestro programa, normalmente mediante un JSON desde endpoints de tipo REST.

Probablemente esta sea la causa más frecuente de errores de integración entre el front y el backend, porque si la estructura de los datos (el “schema”) del endpoint cambia inadvertidamente la aplicación caerá, más antes que después.

Algo que se hace habitualmente para controlar este riesgo es añadir algún tipo de programación defensiva, que generalmente es resultado de algún cambio correctivo previo.

Ya sin contar con lo que haya costado depurar el error en primer término, estas técnicas además son ineficaces: primero, por implicar una sobre ingeniería difícil de justificar, si se aplican a priori antes del fallo; y, segundo, porque no garantizan que se evite repetir la misma situación en el futuro con otros orígenes de datos (¡o incluso el mismo!).

Lo ideal sería que pudiéramos validar que lo entregado por cada endpoint y en cada ocasión sigue cumpliendo con el tipo esperado, o generar un error controlado en caso contrario.

Algo así cortaría de raíz todos los errores derivados de su detección tardía, no ya en desarrollo sino también en producción y desde el mismo instante en que cambie el endpoint.

La solución

Unas veces es el frontend quien define los tipos que necesita para modelar el dominio del problema y el backend así los provee. En otras, los datos ya vienen estructurados desde el backend, al tratar con un API preexistente.

Para definirlos en Typescript existen herramientas online que ayudan a generar automáticamente los tipos, interfaces y clases a partir de un JSON de muestra, como json2ts o quicktype.

Con nuestros tipos ya definidos el objetivo es entonces validar si un JSON determinado cumple con ellos. Para eso existen los JSON Schemas.

Esta es una especificación que permite definir aspectos como las propiedades que definen un tipo, si son obligatorias u opcionales, valores o formatos válidos, añadir documentación y ejemplos… la lista de posibilidades es larga y aquí tienes más información.

Hay herramientas online que permiten entregar un JSON y que te generen su Schema. O, al revés, que entregando un Schema, se pueda validar manualmente un JSON.

Finalmente, vamos al tema más interesante: cómo automatizar todo lo anterior.

El paquete npm typescript-json-validator nos permite automatizar dos de las cosas mencionadas anteriormente:

  1. Generar en tiempo de compilación los Schemas de nuestros tipos Typescript.
  2. Poder validar en tiempo de ejecución cualquier JSON contra ellos.

Partiendo de tener ya definidos los tipos Typescript, la herramienta genera un fichero .ts que exporta una función validate() para uno o varios de nuestros tipos.

Por ejemplo, suponiendo un fichero cita-concertada-endpoint.ts con este contenido:

export interface CitaConcertadaEndpoint {
  idInstalacion: number;
  idEncargo: number;
  matricula: string;
}

Solo habría que abrir una consola donde se encuentre y teclear:

npx typescript-json-validator ./cita-concertada-endpoint.ts CitaConcertadaEndpoint

El código generado incluirá un JSON Schema para nuestro tipo que, opcionalmente, podrías afinar con herramientas como las descritas anteriormente.

Así sería posible, si añades las anotaciones adecuadas, incorporar muchos elementos que la herramienta no podría deducir por sí misma, como valores por defecto, formatos (como que un string sea una fecha o un email válidos) o rangos, entre otros muchos.

Tienes más información sobre esas posibles anotaciones aquí.

Luego, en tiempo de ejecución, en nuestro programa validaríamos lo entregado por el endpoint REST de esta manera:

import validate from './cita-concertada-endpoint.validator.ts';

let validatedValue: CitaConcertadaEndpoint;
try {
   validatedValue = validate(json);  // El dato a validar
} catch(err) {
   console.log(err);
   throw err;
}

return validatedValue;

Si además estructuramos por capas nuestra aplicación y una capa de acceso a datos que incluya esta validación, entonces tendríamos la seguridad de que, por encima de ella, toda la información se ajustaría a los tipos que hemos definido, pudiendo eliminar cualquier tipo de programación defensiva al respecto.

Happy coding!

Notas

npm install ajv
yarn add ajv

Cuéntanos qué te parece.

Los comentarios serán moderados. Serán visibles si aportan un argumento constructivo. Si no estás de acuerdo con algún punto, por favor, muestra tus opiniones de manera educada.

Suscríbete

Estamos comprometidos.

Tecnología, personas e impacto positivo.