Validación de la interfaz TypeScript con Joi

La historia de cómo pasar dos días reescribiendo el mismo código varias veces.


Joi & TypeScript. A love story


Entrada


En este artículo, omitiré detalles sobre Hapi, Joi, enrutamiento y validate: { payload: ... } , lo que implica que ya comprende de qué se trata, así como la terminología, a la "interfaces", "tipos" y similares . Solo te contaré sobre una estrategia por turnos, no la más exitosa, mi entrenamiento en estas cosas.


Un poco de historia


Ahora soy el único desarrollador de back-end (es decir, escribir código) en el proyecto. La funcionalidad no es la esencia, pero la esencia clave es un perfil bastante largo con datos personales. La velocidad y la calidad del código se basan en mi poca experiencia de trabajar de forma independiente en proyectos desde cero, incluso menos experiencia trabajando con JS (solo el cuarto mes), y en el camino, muy torcidamente oblicuamente, escribo en TypeScript (en adelante, TS). Las fechas se comprimen, los rollos se comprimen, las ediciones llegan constantemente y resulta que primero se escribe el código de lógica de negocios y luego las interfaces en la parte superior. Sin embargo, un deber técnico es capaz de ponerse al día y tocar la tapa, lo que, aproximadamente, nos ha sucedido.


Después de 3 meses de trabajo en el proyecto, finalmente estuve de acuerdo con mis colegas para cambiar a un solo diccionario para que las propiedades del objeto se nombraran y escribieran igual en todas partes. Bajo este negocio, por supuesto, me comprometí a escribir una interfaz y me quedé atascado firmemente durante dos días hábiles.


El problema


Un perfil de usuario simple será un ejemplo abstracto.


  • Primero El paso cero de un buen desarrollador: describir datos escribir pruebas;
  • Primer paso: escribir pruebas Describir datos.
  • Y así sucesivamente.

Supongamos que ya se han escrito pruebas para este código, queda por describir los datos:


 interface IUser { name: string; age: number; phone: string | number; } const aleg: IUser = { name: 'Aleg', age: 45, phone: '79001231212' }; 

Bueno, aquí todo es claro y extremadamente simple. Todo este código, como recordamos, en el backend, o más bien, en la API, es decir, el usuario se crea en función de los datos que llegaron a través de la red. Por lo tanto, debemos validar los datos entrantes y ayudar a Joi en esto:


 const joiUserValidator = { name: Joi.string(), age: Joi.number(), phone: Joi.alternatives([Joi.string(), Joi.number()]) }; 

La solución "en la frente" está lista. El inconveniente obvio de este enfoque es que el validador está completamente divorciado de la interfaz. Si durante la vida de la aplicación los campos cambian / agregan o su tipo cambia, entonces este cambio deberá ser rastreado manualmente e indicado en el validador. Creo que no habrá desarrolladores tan responsables hasta que algo caiga. Además, en nuestro proyecto, el cuestionario consta de más de 50 campos en tres niveles de anidación y es extremadamente difícil de entender, incluso sabiendo todo de memoria.


Simplemente no podemos especificar const joiUserValidator: IUser , porque Joi usa sus propios tipos de datos, lo que genera errores cuando el tipo de compilación Type 'NumberSchema' is not assignable to type 'number' . ¿Pero debe haber una manera de realizar la validación en la interfaz?


Tal vez no lo busqué en Google correctamente, o estudié mal las respuestas, pero todas las decisiones se redujeron a extractTypes y algún tipo de bicicletas feroces, como esta :


 type ValidatedValueType<T extends joi.Schema> = T extends joi.StringSchema ? string : T extends joi.NumberSchema ? number : T extends joi.BooleanSchema ? boolean : T extends joi.ObjectSchema ? ValidatedObjectType<T> : /* ... more schemata ... */ never; 

Solución


Use bibliotecas de terceros


Por qué no Cuando le pregunté a la gente sobre mi tarea, recibí en una de las respuestas, y más tarde, y aquí, en los comentarios (gracias a keenondrums ), enlaces a estas bibliotecas:
https://github.com/typestack/class-validator
https://github.com/typestack/class-transformer


Sin embargo, había un interés en resolverlo usted mismo, comprender mejor el trabajo de TS, y nada era urgente para resolver el problema momentáneamente.


Obtén todas las propiedades


Como no tenía ningún trabajo previo con estática, el código anterior descubrió América en términos de usar operadores ternarios en tipos. Afortunadamente, no fue posible aplicarlo en el proyecto. Pero encontré otra bicicleta interesante:


 interface IUser { name: string; age: number; phone: string | number; } type UserKeys<T> = { [key in keyof T]; } const evan: UserKeys<IUser> = { name: 'Evan', age: 32, phone: 791234567890 }; const joiUser: UserKeys<IUser> = { name: Joi.string(), age: Joi.number(), phone: Joi.alternatives([Joi.string(), Joi.number()]) }; 

TypeScript condiciones bastante complicadas y misteriosas, le permite obtener, por ejemplo, claves de la interfaz, como si fuera un objeto JS normal, aunque solo en la construcción de type y mediante la key in keyof T y solo a través de genéricos. Como resultado del tipo UserKeys , todos los objetos que implementan las interfaces deben tener el mismo conjunto de propiedades, pero los tipos de valores pueden ser arbitrarios. Esto incluye sugerencias en el IDE, pero aún no da una indicación clara de los tipos de valores.


Aquí hay otro caso interesante que no pude usar. Quizás pueda decirme por qué esto es necesario (aunque supongo que parcialmente, no hay suficiente ejemplo aplicado):


 interface IUser { name: string; age: number; phone: string | number; } interface IUserJoi { name: Joi.StringSchema, age: Joi.NumberSchema, phone: Joi.AlternativesSchema } type UserKeys<T> = { [key in keyof T]: T[key]; } const evan: UserKeys<IUser> = { name: 'Evan', age: 32, phone: 791234567890 }; const userJoiValidator: UserKeys<IUserJoi> = { name: Joi.string(), age: Joi.number(), phone: Joi.alternatives([Joi.string(), Joi.number()]) }; 

Usar tipos variables


Puede establecer explícitamente los tipos, y usando "OR" y extrayendo las propiedades, obtenga un código que funcione localmente:


 type TString = string | Joi.StringSchema; type TNumber = number | Joi.NumberSchema; type TStdAlter = TString | TNumber; type TAlter = TStdAlter | Joi.AlternativesSchema; export interface IUser { name: TString; age: TNumber; phone: TAlter; } type UserKeys<T> = { [key in keyof T]; } const olex: UserKeys<IUser> = { name: 'Olex', age: 67, phone: '79998887766' }; const joiUser: UserKeys<IUser> = { name: Joi.string(), age: Joi.number(), phone: Joi.alternatives([Joi.string(), Joi.number()]) }; 

El problema de este código se manifiesta cuando queremos recoger un objeto válido, por ejemplo, de la base de datos, es decir, TS no sabe de antemano qué tipo de datos serán: simples o Joi. Esto puede causar un error al intentar realizar operaciones matemáticas en un campo que se espera como number :


 const someUser: IUser = getUserFromDB({ name: 'Aleg' }); const someWeirdMath = someUser.age % 10; // error TS2362: The left-hand side of an arithmetic operation must be of type'any', 'number', 'bigint' or an enum type 

Este error proviene de Joi.NumberSchema porque la edad no puede ser solo un number . Por lo que lucharon y se encontraron.


¿Combinar dos soluciones en una?


En algún punto en este punto, la jornada laboral se acercaba a su conclusión lógica. Respiré, bebí café y borré la puta mierda. ¡Es necesario leer estos tu Internet menos! El tiempo ha llegado toma una escopeta y lavado de cerebro


  1. Un objeto debe formarse con tipos de valores explícitos;
  2. Puede usar genéricos para lanzar tipos en una sola interfaz;
  3. Los genéricos admiten tipos predeterminados;
  4. El type construcción es claramente capaz de otra cosa.

Escribimos la interfaz genérica con los tipos predeterminados:


 interface IUser < TName = string, TAge = number, TAlt = string | number > { name: TName; age: TAge; phone: TAlt; } 

Para Joi, podría crear una segunda interfaz, heredando la principal de esta manera:


 interface IUserJoi extends IUser < Joi.StringSchema, Joi.NumberSchema, Joi.AlternativesSchema > {} 

No es lo suficientemente bueno, porque el próximo desarrollador puede expandir IUserJoi con un corazón ligero o peor. Una opción más limitada es obtener un comportamiento similar:


 type IUserJoi = IUser<Joi.StringSchema, Joi.NumberSchema, Joi.AlternativesSchema>; 

Intentamos:


 const aleg: IUser = { name: 'Aleg', age: 45, phone: '79001231212' }; const joiUser: IUserJoi = { name: Joi.string(), age: Joi.number(), phone: Joi.alternatives([Joi.string(), Joi.number()]) }; 

UPD:
Para terminar en Joi.object tuve que luchar con el error TS2345 y la solución más simple fue as any . Creo que esto no es una suposición crítica, porque el objeto anterior todavía está en la interfaz.


 const joiUserInfo = { info: Joi.object(joiUser as any).required() }; 

Se compila, se ve bien en el lugar de uso y, en ausencia de condiciones especiales, siempre establece los tipos predeterminados. Belleza ...
-
... lo que pasé dos días hábiles


Resumen


¿Qué conclusiones se pueden sacar de todo esto?


  1. Obviamente, no aprendí a encontrar respuestas a las preguntas. Seguramente, con una solicitud exitosa, esta solución (o incluso mejor) se encuentra en los primeros 5k enlaces del motor de búsqueda;
  2. Cambiar al pensamiento estático de dinámico no es tan fácil, mucho más a menudo simplemente martillo en tal enjambre;
  3. Los genéricos son geniales. En Habr y stackoverflow está lleno de bicicletas soluciones no obvias para construir tipos fuertes ... fuera del tiempo de ejecución.

Lo que ganamos:


  1. Al cambiar la interfaz, todo el código se cae, incluido el validador;
  2. En el editor, aparecieron sugerencias sobre nombres de propiedades y tipos de valores de objetos para escribir un validador;
  3. La falta de oscuras bibliotecas de terceros para el mismo propósito;
  4. Las reglas de Joi se aplicarán solo cuando sea necesario, en otros casos, tipos predeterminados;
  5. Si alguien quiere cambiar el tipo del valor de alguna propiedad, con la organización correcta del código, caerá en el lugar donde se reúnen todos los tipos asociados con esta propiedad;
  6. Aprendimos a ocultar bella y simplemente genéricos detrás de la abstracción de type , descargando visualmente el código de construcciones monstruosas.

Moraleja: la experiencia no tiene precio; para el resto, hay un mapa mundial.


Puedes ver, tocar, ejecutar el resultado final:
https://repl.it/@Melodyn/Joi-by-interface

Source: https://habr.com/ru/post/450238/


All Articles