Haciendo TypeScript más estricto. Informe Yandex

¿Cómo hacer de TypeScript un compañero estricto pero justo que lo protegerá de errores desagradables y le dará más confianza en su código? Alexey Veselovsky veselovskiyaiconsideró varias características de la configuración TS que hacen la vista gorda ante libertades imperdonables. El informe describe las cosas que es mejor evitar y aquellas con las que debe tener mucho cuidado. Aprenderá acerca de la maravillosa biblioteca io-ts: le permite detectar fácilmente e incluso evitar que ingresen datos en el código que pueden causar errores en lugares perfectamente escritos.



- Hola a todos, mi nombre es Lesha, soy un desarrollador frontend. Empecemos. Te contaré un poco sobre mí y el proyecto en el que trabajo. Flow está aprendiendo inglés con Yandex.Practicum. El lanzamiento tuvo lugar en abril de este año. El frente se escribió directamente en TypeScript, antes de eso no había código.







Un poco sobre mi experiencia. En un año lejano, comencé a programar. Un año en 2013, comenzó a trabajar.







Casi de inmediato me di cuenta de que estaba mucho más interesado en el frente, pero tenía experiencia con lenguajes tipados estáticamente. Comencé a usar JavaScript y esta escritura estática no estaba allí. Me pareció conveniente, me gustó.



En un cambio de proyecto, me puse a trabajar usando TypeScript. Te hablaré de las ventajas de las que me di cuenta al cambiar a TypeScript. Más fácil de entender el proyecto. Tenemos una descripción de los tipos de datos que se utilizan en el proyecto y las conversiones entre ellos.







Es más seguro realizar cambios en el código: cuando hay cambios en el backend o solo en alguna parte del código, TypeScript resaltará los lugares donde han aparecido errores.



Hay menos preocupación por los tipos. Cuando creamos una nueva funcionalidad, establecemos inmediatamente los tipos con los que funcionan las funciones, y podemos estar menos preocupados de recibir datos diferentes.



No hay miedo de que llegue nulo o indefinido, no necesitamos ser paranoicos, insertar si innecesarios y construcciones similares.







A principios de este año, me mudé a Flow. TypeScript también se usa aquí, pero no lo reconocí un poco. ¿Por qué? Fue demasiado amable conmigo, una cuarta parte de los errores de los clientes estaban relacionados con nulos e indefinidos. Comencé a averiguar cuál era el problema y encontré una línea en la configuración que cambió todo el comportamiento de TypeScript.







Esta es la inclusión de estricto. No estaba allí, pero era necesario activarlo para mejorar la verificación.



TypeScript: estricto



¿Qué es estricto? ¿En qué consiste?







Este es un conjunto de banderas que se pueden activar individualmente, pero en mi opinión, todas son muy útiles. noImplicitAny: antes de habilitar esta bandera, podemos declarar, por ejemplo, funciones cuyos parámetros serán implícitos, como any. Si habilitamos esta bandera, entonces debemos agregar escritura en lugares donde TypeScript no puede calcular el tipo a partir del contexto.



Es decir, en el segundo caso, debemos agregar mecanografía, ya que no existe contexto como tal. En el tercer caso, donde tenemos un mapa, no podemos agregar tipeo para a, porque está claro por el contexto que habrá un tipo de número.







noImplicitThis. TypeScript nos obliga a escribir esto cuando no hay contexto. Cuando el contexto es, es decir, un objeto o una clase, no es necesario que hagamos esto.







alwaysStrict. Agrega "uso estricto" a cada archivo. Pero afecta la forma en que JavaScript ejecuta nuestro código. (...)







estrictoBindCallApply. Por alguna razón, antes de habilitar esta opción, TypeScript no comprueba vincular, aplicar ni llamar a tipos. Después de encenderlo, los revisa y no nos permite hacer cosas tan desagradables.







StrictNullChecks es, en mi opinión, el control más necesario. Nos obliga a indicar en la mecanografía los lugares donde pueden venir nulos o indefinidos. Antes de la inclusión, podemos pasar nulo o indefinido donde no se especifica explícitamente y, en consecuencia, obtener un error. Después de eso, el control será mucho mejor.







A continuación, StrictFunctionTypes. La situación aquí es un poco más complicada. Imaginemos que tenemos tres funciones. Uno trabaja con animales, otro con perros y otro con gatos. Un perro y un gato son animales. Es decir, será erróneo trabajar con un perro de la misma forma que con un gato, porque son diferentes. Funcionará correctamente con un perro como con un animal.



La tercera opción es cuando intentamos trabajar con cualquier animal como un perro. Por alguna razón, inicialmente está permitido en TypeScript, pero si habilita esta opción, no será válida y se realizarán ciertas verificaciones.







A continuación, strictPropertyInitialization. Esto es para clases. Nos obliga a establecer valores iniciales ya sea al declarar una propiedad o en un constructor. A veces, hay ocasiones en las que necesita eludir esta regla. Puede utilizar un signo de exclamación, pero, de nuevo, esto nos obliga a tener un poco más de cuidado.



Entonces, me di cuenta de que necesitamos habilitar estricto. Intento encenderlo y aparecen muchos errores. Por lo tanto, se decidió utilizar una configuración de transición a estricta. Establecemos estrictos en tres pasos.







La primera etapa: agregamos “estricto”: verdadero a tsconfig y, en consecuencia, nuestro entorno de desarrollo nos solicita lugares con un error, que es causado por la inclusión de estricto.



Pero para el paquete web, creamos un tsconfig especial, que estricto será falso, y lo usamos al compilar. Es decir, nada se rompe durante el montaje, pero en nuestro editor vemos estos errores. Y podemos solucionarlos de inmediato. Luego pasamos de vez en cuando a la segunda etapa, esto es una solución. Construimos nuestro proyecto con el tsconfig habitual. Corregimos algunos de los errores que han salido, y repetimos todo esto en nuestro tiempo libre.



Con tales acciones, hasta ahora hemos reducido el número de nuestros errores de 400 a 200. Estamos deseando pasar a la tercera etapa: eliminar webpackTsConfig y usar tsconfig al compilar, pero con estricto habilitado.



TypeScript:



Puedes hablar un poco sobre las pequeñas sutilezas de TypeScript que no están cubiertas por estrictas, pero son difíciles de formalizar correctamente.







Comencemos con el operador de signo de exclamación. ¿Qué te permite hacer? En este caso, haga referencia a un campo que puede estar indefinido, como si no pudiera estar indefinido. Tiene sentido en modo estricto, cuando intentamos acceder a un campo, diciendo explícitamente: Estoy seguro de que definitivamente no es nulo o indefinido. Pero esto es malo, porque si de repente resulta ser nulo o indefinido, naturalmente obtenemos un error de tiempo de ejecución.



ESLint nos ayudará a evitar tales cosas, simplemente nos prohibirá. Lo hicimos. ¿Cómo soluciono ahora el ejemplo anterior?



Supongamos que tenemos esta situación.







Hay un elemento, puede ser de tipo link o span. Con nuestra cabeza entendemos que span es solo texto, y enlace es texto y enlace.



(imagen)



Pero nos olvidamos de decirle al lenguaje TypeScript, entonces en la función getItemHtml surge una situación que en el caso del enlace tenemos que decir: href no es opcional, definitivamente lo será. Este también es un lugar potencial para errores. ¿Como arreglarlo?







La primera opción es corregir la escritura, es decir, indicar explícitamente a TypeScript que se requiere un href para un enlace y un opcional para span.







Y el signo de exclamación no será necesario aquí.







Segunda opción de corrección. Supongamos que no describimos el tipo de artículo y no podemos limitarnos a tomarlo y restringirlo. Entonces podemos reescribirlo de manera similar.







Tenga en cuenta: el cheque acaba de aparecer. Luego viene el registro de que el programador no esperaba este valor al escribir este código, por lo que en el futuro veremos este error y tomaremos las medidas necesarias.



A continuación, estamos tratando de representar de alguna manera nuestro Item. Aquí simplemente puede darle al usuario un error. Pero si se trata de datos insignificantes, puede crear un código auxiliar como aquí.



como





Más lejos. También hay un operador como. ¿Qué te permite hacer?







Te permite decir: lo sé mejor, hay tal y tal tipo, y también llevarte a un error.



Matrices



Los métodos de lucha son los mismos. Con lo que debes tener un poco más de cuidado son las matrices. TypeScript no es una panacea, no comprobará algunos puntos. Por ejemplo, podemos referirnos a un elemento de matriz inexistente. En este caso, tomaremos el primer elemento de la matriz y obtendremos un error en este código. como podemos arreglar esto?







Nuevamente, hay dos formas. La primera forma es escribiendo. Decimos que tenemos el primer elemento y nos referimos sin miedo a este elemento. O comprobaremos, registraremos, si algo está mal de repente, si esperamos explícitamente una matriz no vacía.



Objetos



Lo mismo ocurre con los objetos. Podemos declarar un objeto, que puede tener cualquier número de propiedades, y también obtener un error indefinido.







Nuevamente, puede dar instrucciones explícitas sobre las propiedades que se requieren o simplemente verificar.



ninguna



Ahora lo obvio es cualquiera.







Le permite hacer referencia a cualquier propiedad de un objeto como si no hubiera nada escrito. En este caso, podemos hacer lo que queramos con x. Y vuelve a dispararte en el pie, comete errores.



Nuevamente, es mejor no permitir esto explícitamente con ESLint. Pero hay situaciones en las que aparece por sí solo.







Por ejemplo, en este caso JSON.parse produce solo este tipo any. ¿Qué se puede hacer?







Puedes simplemente decir: No te creo, mejor digamos que no sé qué es y viviré con eso. ¿Cómo vivir con eso? He aquí un ejemplo hipotético.







Hay un usuario, el usuario tiene un nombre requerido y un correo electrónico opcional.







Estamos escribiendo la función parseUser. Toma una cadena JSON y nos devuelve nuestro objeto. Ahora empezamos a comprobar todo esto. Primero, vemos la línea con parse y desconocido que nos es familiar de la diapositiva anterior. A continuación, comenzamos a verificar.







Si no es un objeto o es nulo, arroja un error.







Además, si no hay una propiedad de nombre requerida o no es una cadena, arrojamos un error. Aquí está la continuación del código.







Empezamos a formar Usuario, ya que ya se han recopilado todos los campos obligatorios.







A continuación, comprobamos si hay un campo de correo electrónico. Si es así, comprobamos su tipo y, si el tipo no coincide, arrojamos un error. Si no hay correo electrónico, entonces no enviamos nada y devolvemos el resultado. Todo esta bien. Pero necesitas escribir mucho para el tipo más simple.







Y se necesitan muchos controles



Necesitamos mucha validación porque una solicitud JSON típica se ve así.







Sin más preámbulos, esto es solo buscar y json (). La conversión de any a SomeRequestResponse aparece a cambio. Esto también debe combatirse. Puedes hacerlo de la forma anterior, o puedes hacerlo un poco diferente.



io-ts



Es lo mismo bajo el capó: usamos una biblioteca especial para la verificación de tipos. En este caso, es io-ts. Aquí hay un ejemplo simple de cómo trabajar con él.







Tomemos el tipo de usuario anterior y escribamos dentro de la biblioteca que estamos usando. Sí, escribir aquí es un poco más complicado, pero se deben cumplir dos condiciones simultáneamente. Debe ser un objeto con un campo de nombre obligatorio y un objeto con un campo de correo electrónico opcional. ¿Cómo podemos comprobar todo esto?







Escribamos el mismo parseUser. En este caso, estamos utilizando el método User.decode. Pasamos el objeto ya emparejado allí, nos devuelve el resultado. Quizás en un formato inusual. Un objeto de tipo O bien, puede estar en dos estados. El primero es correcto. Por lo general, esto significa que todo salió bien. izquierda dice que no fue muy bien. Ambos estados tienen propiedades que nos permiten aprender más. Si tiene éxito, este es el resultado de la ejecución, en caso de error, un error.



Comprobamos si nuestros resultados están en el estado izquierdo. Si es así, decimos que se ha producido un error. Luego, si todo está bien, simplemente devolvemos el resultado.



Visualización de errores







Acerca de mostrar errores. Puedes mejorarlo un poco. Usaremos io-ts-reporters para esto. Es una biblioteca escrita por el mismo autor que io-ts. Permite que el error se presente maravillosamente. ¿Que hace ella? Cambiamos el código aquí donde está el pato. Toma el resultado y devuelve una matriz de cadenas. Simplemente lo unimos en una línea y lo mostramos. ¿Qué obtenemos al final?







Supongamos que pasamos un valor nulo a una cadena JSON.







Dará dos errores. Esto se debe a la sutileza de la implementación, porque hicimos intersección. Los errores son bastante claros. Ambos dicen que esperábamos un objeto pero obtuvimos un valor nulo. Es solo que para cada una de estas condiciones dará un error por separado.







A continuación, intentemos pasar una matriz vacía allí. Será lo mismo.







Simplemente nos dirá: también esperaba un objeto, pero recibí una matriz vacía.







Entonces, seguimos viendo qué pasará si comenzamos a transmitir datos incorrectos. Por ejemplo, pasemos un objeto vacío.







Ahora dará un error sobre el hecho de que no tenemos el campo de nombre requerido. Esperaba que el campo de nombre fuera de tipo cadena, pero termina indefinido. También es fácil entender a partir de este error lo que sucedió.







A continuación, intentaremos pasar un tipo incorrecto allí. También obtenemos un error, aproximadamente el mismo que en el ejemplo anterior.







Pero aquí nos escribe claramente el significado que le transmitimos.







¿Qué más pueden hacer los io-ts? Le permite obtener un tipo de TypeScript. Es decir, agregamos esta línea. Simplemente agregando typeof, también typeof, obtenemos un tipo TypeScript que podemos usar más en la aplicación. Convenientemente.







¿Qué más puede hacer esta biblioteca? Convertir tipos. Digamos que hacemos una solicitud al servidor. El servidor envía las fechas en formato de hora Unix. Y hay una biblioteca especial, nuevamente del creador de la biblioteca io-ts: io-ts-types. Hay transformaciones que se escribieron originalmente y herramientas para facilitar la escritura de esas transformaciones. Agregamos un campo de fecha: proviene del servidor como un número, y terminamos recibiéndolo como un objeto Date.



Describamos el tipo



Veamos qué hay dentro de esta biblioteca e intentemos describir el tipo más simple.







Primero, veamos cómo se describe generalmente. Se describe de la misma manera, bastante complicado, dado que también es necesario para las transformaciones. Aparte del servidor al cliente, si consideramos la interacción con el servidor, y la transformación inversa, del cliente al servidor.



Simplifiquemos un poco nuestra tarea. Simplemente escribiremos el tipo que marca. En este caso, averigüemos qué significan estos campos. nombre: escriba el nombre.







Es necesario para mostrar errores. Como vimos en los ejemplos anteriores, los errores deletrean de alguna manera el nombre del tipo. Puedes especificarlo aquí.



A continuación, está la función de validación. Toma, digamos, del servidor, el valor desconocido; toma un contexto para mostrar correctamente el error; y devuelve un objeto Either en dos estados: un error o un valor validado.



Hay dos funciones más: es y codificar. Se utilizan para transformarlos a la inversa, pero no los toquemos por ahora.







¿Cómo se puede representar el tipo de cadena más simple? Establecemos el nombre en cadena y verificamos que sea una cadena. Con una conversión directa, esto no será necesario, pero formalmente lo escribimos. Y luego simplemente hacemos typeof para verificar. Si tiene éxito, devolvemos el resultado correcto y, como resultado de un error, fracaso. También se agrega el contexto para que el error se muestre correctamente. Y simplemente devolvemos lo mismo, porque no hay transformación inversa.



En la práctica



¿Qué hay en práctica? ¿Por qué decidimos verificar los datos que provienen del servidor?







Como mínimo, hay JSON en la base de datos. Nosotros, por supuesto, creemos que estará bien dirigido y que será revisado en algunos puntos. Pero el formato puede cambiar un poco, no debemos romper la interfaz ni descubrir de inmediato los errores para tomar medidas.



Tenemos Python en el servidor sin escribir explícitamente. A veces puede haber pequeños problemas con esto. Y para no averiarnos, simplemente podemos comprobar y asegurarnos adicionalmente, por si acaso.



No hay documentación clara sobre las respuestas del servidor. Probablemente, el servidor está más preocupado por lo que le vendrá que por lo que dará. Sí, este es más nuestro problema: no romper.







¿Qué encontramos? Ya hemos empezado a usarlo un poco. Descubrí que el servidor nos da un objeto vacío en lugar de una matriz vacía. Acabo de revisar el código, está escrito para devolver un objeto vacío.



Además, la ausencia de algunos campos. Pensamos que eran obligatorios, pero resultan ser opcionales.



En algunos casos, simplemente faltaba un campo que aceptaba valores NULL. Es decir, un campo opcional se puede presentar de dos formas: cuando simplemente no lo pasamos o cuando pasamos nulo. Tampoco siempre nos llegó correctamente. Para no detectar errores en medio de nuestro código, podemos detectar esto solo en las solicitudes.







¿Qué tenemos ahora? Ya hemos comprobado muchas respuestas del servidor y hemos registrado si algo no nos gusta. Luego analizamos esto y establecemos tareas: ya sea para cambiar la escritura en nuestro frontend o ediciones en el backend. Ahora no cambiamos los datos que vienen del servidor: si vino nulo en lugar de una cadena, no lo cambiamos, por ejemplo, a una cadena vacía.



Nuestros planes son verificar y registrar, pero corregir si hay un error. Si recibimos datos incorrectos, corregiremos este valor para que los usuarios puedan mostrar al menos algo en lugar de caer dentro de nuestro código.







Pequeños resultados. Activamos estricto para que TypeScript nos ayude más, excluir como, cualquiera y el signo de exclamación. Tendremos más cuidado con las matrices y los objetos en TypeScript, y también comprobaremos todos los datos externos. Por cierto, estos no son solo servidores. También puede consultar localStorage, mensajes que vienen en eventos. Por ejemplo, postMessage.



Gracias por la atención.



All Articles