Derivar un tipo de acción usando el mecanografiado

Hola a todos! Mi nombre es Dmitry Novikov, soy un desarrollador de JavaScript en Alfa Bank, y hoy les contaré sobre nuestra experiencia en derivar el tipo de acción usando el Script mecanográfico, qué problemas encontramos y cómo los resolvimos.

Esta es una transcripción de mi informe sobre Alfa JavaScript MeetUp. Puede ver el código de las diapositivas de la presentación aquí , y la grabación de la transmisión de mitap aquí .

Nuestras aplicaciones front-end se ejecutan en un montón de React + Redux. El flujo de datos de Redux simplemente se ve así:


Hay creadores de acciones, funciones que devuelven una acción. Las acciones caen en el reductor, el reductor crea un nuevo lado basado en el anterior. Los componentes se firman a la parte, que a su vez puede enviar nuevas acciones, y todo se repite.

Así es como se ve el creador de la acción en el código:


Esta es solo una función que devuelve una acción: un objeto que debe tener un campo de tipo cadena y algunos datos (opcional).

Así es como se ve un reductor típico:


Este es un caso de cambio regular que mira el campo de tipo de una acción y genera un nuevo lado. En el ejemplo anterior, simplemente agrega los valores de propiedad de la acción allí.

¿Qué pasa si accidentalmente cometemos un error al escribir un reductor? Por ejemplo, así, intercambiaremos las propiedades de diferentes acciones:


Javascript no sabe nada sobre nuestras acciones y considera que dicho código es absolutamente válido. Sin embargo, no funcionará según lo previsto, y nos gustaría ver este error. ¿Qué nos ayudará si no mecanografia? Tratemos de tipificar nuestras acciones.


Para empezar, escribiremos tipos de "frente" para nuestras acciones: Action1Type y Action2Type. Y luego, combínelos en un tipo de unión para usar en el reductor. El enfoque es simple y directo, pero ¿qué pasa si los datos en las acciones cambian durante el desarrollo de la aplicación? No cambie los tipos manualmente cada vez. Los reescribimos de la siguiente manera:


El operador typeof nos devolverá el tipo de creador de acción, y ReturnType nos dará el tipo del valor de retorno de la función, es decir, tipo de acción Como resultado, resultará igual que la diapositiva anterior, pero ya no será manual; al cambiar las acciones, los tipos de acción tipo unión se actualizarán automáticamente. Wow! Lo escribimos en el reductor y ...


E inmediatamente obtenemos errores del script. Además, los errores no están del todo claros: la propiedad de la barra está ausente en la acción foo, y la ausencia de foo en la barra ... ¿Parece ser la forma en que debería ser? Algo parece estar en mal estado. En general, el enfoque de la frente no funciona como se esperaba.

Pero este no es el único problema. Imagine que con el tiempo, nuestra aplicación crecerá y tendremos muchas acciones. Mucho


¿Cómo sería nuestro tipo común para ellos en este caso? Probablemente algo como esto:


Y si tenemos en cuenta que las acciones se agregarán y eliminarán, tendremos que admitir todo esto manualmente: agregar y eliminar tipos. Esto tampoco nos conviene en absoluto. Que hacer Comencemos con el primer problema.



Entonces, tenemos un par de creadores de acción, y el tipo común para ellos es la unión de tipos de acción derivados automáticamente. Cada acción tiene una propiedad de tipo y se define como una cadena. Esta es la raíz del problema. Para distinguir una acción de otra, necesitamos que cada tipo sea único y solo acepte un valor único.



Este tipo se llama literal. El tipo literal es de tres tipos: numérico, cadena y booleano.



Por ejemplo, tenemos el tipo onlyNumberOne y especificamos que una variable de este tipo solo puede ser igual al número 1. Asigne 2 y obtenga un error de typscript. La cadena funciona de manera similar: solo se puede asignar un valor de cadena específico a una variable. Bueno, booleano es verdadero o falso, sin ambigüedad.

Genérico


¿Cómo guardar este tipo sin permitir que se convierta en una cadena? Usaremos genéricos. Genérico es tal abstracción sobre los tipos. Supongamos que tenemos una función inútil que toma una entrada como argumento y la devuelve sin cambios. ¿Cómo puedo escribirlo? Escribe cualquiera, porque puede ser absolutamente cualquier tipo? Pero si hay algún tipo de lógica presente en la función, puede ocurrir una conversión de tipo y, por ejemplo, un número puede convertirse en una cadena, y cualquier combinación se saltará esto. No es adecuado



Un genérico nos ayudará a salir de esta situación. La entrada anterior significa que estamos pasando un argumento de cierto tipo T, y la función devolverá exactamente el mismo tipo T. No sabemos cuál será: un número, una cadena, booleano u otra cosa, pero podemos garantizar que Será exactamente del mismo tipo. Esta opción nos conviene.

Desarrollemos un poco el concepto de genéricos. Necesitamos procesar no todos los tipos en general, sino un literal de cadena concreto. Hay una palabra clave extendida para esto:



La notación "T extiende la cadena" significa que T es un tipo determinado, que es un subconjunto del tipo de cadena. Vale la pena señalar que esto solo funciona con tipos primitivos: si en lugar de usar una cadena usaríamos un tipo de objeto con un conjunto específico de propiedades, por el contrario significaría que T es un conjunto OVER de este tipo.

A continuación se muestran ejemplos del uso de una función escrita con extend y generics:


  • Argumento de tipo string: la función devolverá string
  • Un argumento de tipo cadena literal: la función devolverá una cadena literal
  • Si el argumento no se parece a una cadena, por ejemplo, un número o una matriz, el script dará un error.


Bueno, y en general funciona.


Sustituimos nuestra función en el tipo de acción: devuelve exactamente el mismo tipo de cadena, pero ya no es una cadena, sino una cadena literal, como debería ser. Recopilamos el tipo de unión, tipificamos un reductor: todo está bien. Y si cometemos un error y escribimos las propiedades incorrectas, el script de tiempo nos dará no dos, sino uno, error lógico y comprensible:


Vayamos un poco más lejos y abstraigamos del tipo de cadena. Escribiremos la misma tipificación, solo usando dos genéricos: T y U. Ahora tenemos un cierto tipo de T que dependerá de otro tipo de U, en lugar del cual podemos usar cualquier cosa: al menos una cadena, al menos un número, al menos un booleano. Esto se implementa utilizando la función de contenedor:


Y finalmente: el problema descrito colgó durante mucho tiempo como un problema en el github, y finalmente, en la versión 3.4 de TypeScript, los desarrolladores nos presentaron una solución: la afirmación constante. Tiene dos formas de grabación:


Por lo tanto, si tiene un mecanografiado nuevo, simplemente puede usarlo como constante en las acciones, y el tipo literal no se convertirá en una cadena. En versiones anteriores, puede usar el método descrito anteriormente. Resulta que ahora tenemos hasta dos soluciones para el primer problema. Pero el segundo permanece.



Todavía tenemos muchas acciones diferentes, y a pesar del hecho de que ahora sabemos cómo manejar sus tipos correctamente, todavía no sabemos cómo ensamblarlos automáticamente. Podemos escribir la unión manualmente, pero si las acciones se eliminan y agregan, aún necesitamos eliminarlas manualmente y agregarlas en el tipo. Esto esta mal.


Por donde empezar Supongamos que tenemos creadores de acciones importados juntos desde un solo archivo. Nos gustaría analizarlos uno por uno, deducir los tipos de sus acciones y agruparlos en un tipo de unión. Y lo más importante, nos gustaría hacer esto automáticamente, sin editar manualmente los tipos.


Comencemos por los creadores de acción. Para hacer esto, hay un tipo mapeado especial que describe las colecciones de valores clave. Aquí hay un ejemplo:


Esto crea un tipo para un objeto cuyas claves son option1 y option2 (del conjunto Keys), y los valores son verdaderos o falsos. En una versión más general, esto se puede representar como un tipo de mapOfBool, un objeto con algún tipo de teclas de fila y valores booleanos.

Bueno Pero, ¿cómo podemos verificar que es un objeto que se nos da en la entrada, y no otro tipo? El tipo condicional, un ternario simple en el mundo de los tipos, nos ayudará con esto.


En este ejemplo, comprobamos: ¿el tipo T tiene algo en común con la cadena? En caso afirmativo, devuelva la cadena y, si no, devuelva nunca. Este es un tipo tan especial que siempre nos devolverá un error. La cadena literal satisface la condición ternaria. Aquí hay algunos códigos de muestra:


Si especificamos algo en los genéricos que no es como una cadena, typecript nos dará un error.

Descubrimos la solución y la verificación, solo queda obtener los tipos y fusionarlos en la unión. Esto nos ayudará a inferir inferencia de tipos en mecanografiado. Infer generalmente vive en un tipo condicional y hace algo como esto: pasa por todos los pares clave-valor, trata de inferir el tipo de valor y lo compara con los demás. Si los tipos de valores son diferentes, los combina en una unión. Justo lo que necesitamos!


Bueno, ahora queda por poner todo junto.

Resulta este diseño:


La lógica es aproximadamente la siguiente: si T parece un objeto que tiene algunas teclas de cadena (nombres de creadores de acciones) y tienen valores de algún tipo (una función que nos devolverá la acción), intente omitir estos pares, deduzca el tipo de estos valores y reducen su tipo común. Y si algo sale mal, descarte un error especial (escriba nunca).

Es difícil solo a primera vista. De hecho, todo es bastante simple. Vale la pena prestar atención a una característica interesante: debido al hecho de que cada acción tiene un campo de tipo único, los tipos de estas acciones no se mantendrán juntos, y obtenemos un tipo de unión completa en la salida. Así es como se ve en el código:


Importamos los creadores de acciones como acciones, tomamos su tipo de retorno (el tipo del valor de retorno es acciones) y recopilamos utilizando nuestro tipo especial. Resulta justo lo que se requería.


Cual es el resultado? Tenemos unión de tipos literales para todas las acciones. Cuando se agrega una nueva acción, el tipo se actualiza automáticamente. Como resultado, obtenemos un mecanografiado estricto de acciones, ahora no podemos cometer un error. Bueno, en el camino, aprendimos sobre genéricos, tipo condicional, tipo mapeado, nunca e inferir: puede obtener aún más información sobre estas herramientas aquí .

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


All Articles