Mantener bibliotecas de JS ampliamente utilizadas ya es complicado, y TypeScript agrega un conjunto adicional de desafíos.
Únete al mantenedor de Redux, Mark Erikson, para conocer algunos de los problemas únicos a los que se enfrentan los mantenedores de bibliotecas de TS y cómo el equipo de Redux ha abordado esos problemas. Cubriremos:
- Compromisos de diferentes formas de definir tipos de TS para una biblioteca
- Cómo apuntar a diferentes versiones de TS y consideraciones para determinar el rango de versiones admitidas
- Migrar bibliotecas de JS existentes a TS
- Diferencias entre escribir tipos de "aplicación" y tipos de "biblioteca"
- Administrar y versionar APIs de tipos públicos
- Consejos y trucos utilizados por los tipos de las bibliotecas de Redux
- Limitaciones de TS y posibles mejoras a nivel de lenguaje
Lecciones de Mantenimiento de Bibliotecas de TypeScript
Video Summary and Transcription
Mark Erickson, un Ingeniero Frontend Senior en Replay, discute las bibliotecas de JavaScript y su soporte para TypeScript, incluyendo migración, versionado y depuración. También explora los desafíos de soportar múltiples versiones de TypeScript y diseñar APIs para su uso con TypeScript. Además, comparte trucos avanzados de tipos de Redux y conocimientos sobre el mantenimiento de una biblioteca de TypeScript. Los resultados de la encuesta revelan el amplio uso de TypeScript entre los desarrolladores, con muchos migrando gradualmente sus bases de código. Por último, proporciona consejos para actualizar TypeScript y verificar la funcionalidad.
1. Introducción a Mark Erickson
Hola, soy Mark Erickson, un Ingeniero Frontend Senior en Replay, conocido por responder preguntas sobre React y Redux, recopilar enlaces útiles, escribir publicaciones en el blog y ser un mantenedor de Redux.
Hola, soy Mark Erickson, y hoy me gustaría hablarles sobre las lecciones que he aprendido al mantener bibliotecas de TypeScript. Un par de cosas rápidas sobre mí. Actualmente soy un Ingeniero Frontend Senior en Replay, donde estamos construyendo un depurador de aplicaciones JavaScript que permite viajar en el tiempo real. Si no lo has visto, por favor échale un vistazo. Soy conocido por varias cosas. Soy un respondedor de preguntas. Con gusto responderé preguntas sobre React y Redux en cualquier lugar donde haya un cuadro de texto en internet. Recolecto enlaces interesantes sobre cualquier cosa que parezca potencialmente útil. Escribo publicaciones de blog extremadamente largas sobre React y Redux, y soy un mantenedor de Redux. Además, es posible que también me conozcas como ese chico con el avatar de los Simpsons.
2. JavaScript Libraries and TypeScript Support
Hoy vamos a hablar sobre las diferentes formas en que las bibliotecas de JavaScript pueden usar y admitir TypeScript, cómo abordar la migración de una biblioteca de JavaScript a TypeScript, los problemas al tratar con los tipos y versiones de la biblioteca, y cómo admitir múltiples versiones de TypeScript. También veremos cómo depurar y probar los tipos, cómo diseñar APIs para su uso con TypeScript y algunas características que facilitarían el uso de TypeScript con bibliotecas. Los tipos cumplen varios propósitos, incluida la documentación de la API, la corrección del código del usuario y la corrección del código de la biblioteca. Existe una diferencia clara entre los tipos de aplicaciones y los tipos de bibliotecas, siendo los tipos de bibliotecas más complejos. Las bibliotecas de JavaScript pueden proporcionar tipos al estar escritas en TypeScript, escribir definiciones de tipos para el código de JavaScript o utilizar tipos de la comunidad de Definitely Typed. Las bibliotecas de Redux han utilizado todos estos enfoques y se han estado migrando a TypeScript. El primer paso en la migración es configurar la infraestructura de compilación y configurar las pruebas.
Entonces, ¿por qué proporcionamos tipos con una biblioteca de todos modos? Los tipos cumplen varios propósitos. Uno de ellos es la documentación de la API. Los usuarios pueden ver los tipos y comprender qué funciones y tipos existen y cómo pueden usarlos en su aplicación. Otro propósito es la corrección del código del usuario. Pueden aplicar ciertos patrones de uso que los usuarios deben tener en su código fuente de la aplicación real. Junto con eso, está la corrección del código de la biblioteca. Los tipos nos ayudan a asegurarnos de que el código dentro de nuestra biblioteca se comporte como se espera y se trata de la mantenibilidad, poder trabajar en el código real dentro de la biblioteca.
Ahora, diré que creo que hay una diferencia clara entre los tipos que se ven dentro del código de la aplicación y los tipos que se ven dentro del código de la biblioteca. Los tipos de la aplicación tienden a ser bastante simples. Tienes respuestas de la API, argumentos de función, estado con el que estás tratando, props de componentes. Por lo general, no es demasiado complicado y no se ven muchos tipos genéricos allí. Los tipos de la biblioteca, por otro lado, son mucho más complicados porque necesitan manejar casos de uso mucho más flexibles. Los tipos de la biblioteca tienden a hacer un uso mucho más intensivo de los genéricos deTypeScript. Y a veces, incluso puedes ver programación a nivel de tipo donde se realiza inferencia, lógica condicional y transformaciones complejas de tipos también.
Ahora, hay varias formas en que una biblioteca de JavaScript puede proporcionar tipos. El mejor enfoque es si la biblioteca está escrita en TypeScript en sí. Esto garantiza que los tipos coincidan con el comportamiento real de lo que está en el código fuente de la biblioteca y que los tipos se actualicen cada vez que haya una nueva versión de la biblioteca. Otra opción es escribir el código fuente en JavaScript y escribir manualmente las definiciones de tipo e incluirlas en la salida publicada. Esto está bien porque los tipos aún son mantenidos por los propietarios reales de la biblioteca. Pero puedes encontrarte en situaciones donde hay diferencias entre lo que los tipos dicen que está en la biblioteca y lo que el código fuente realmente hace. Como último recurso, si los mantenedores de la biblioteca no quieren tener sus propios tipos, entonces la comunidad puede reunir algunos tipos y publicarlos en el repositorio de Definitely Typed propiedad de Microsoft. Esto definitivamente puede causar problemas, pero al menos te brinda una forma de tener tipos, especialmente si los mantenedores de la biblioteca no quieren preocuparse por lidiar con eso ellos mismos. Realmente hemos utilizado todos estos enfoques diferentes con las bibliotecas de Redux a lo largo del tiempo. Pero hemos estado trabajando en tratar de migrar las bibliotecas de Redux a TypeScript, especialmente en los últimos años. El núcleo de Redux se convirtió a TypeScript en 2019. Simplemente nunca llegamos a publicarlo. React Redux versión 8 finalmente se convirtió a TypeScript, y migramos Reselect el año pasado. Entonces, ¿cómo abordas una de estas migraciones? Bueno, el primer paso es configurar la infraestructura de compilación. Debes asegurarte de compilar realmente los tipos de TypeScript, transformar la salida de la compilación a JavaScript simple y asegurarte de que la configuración de tus pruebas esté configurada para trabajar con TypeScript también.
3. Managing Types and Versioning
Es necesario incluir los tipos directamente en el paquete publicado, probar el paquete con código TypeScript, utilizar las definiciones de tipo existentes como punto de partida, convertir archivos a TypeScript, cambiar el nombre de los archivos para preservar el historial de Git, convertir las pruebas a TypeScript, exportar tipos desde el archivo de índice, gestionar la versión de los tipos públicos, dividir los cambios en cambios que rompen y no rompen, apuntar a versiones específicas de TypeScript, admitir versiones antiguas, adoptar nueva sintaxis y características, probar con múltiples versiones de TypeScript.
También debes asegurarte de que los tipos se incluyan directamente en el paquete publicado. Esto significa agregar una clave de tipos a tu archivo package.json. También es una buena idea probar el paquete con código de aplicación TypeScript y asegurarte de que todo funcione como esperas. Me gusta usar una herramienta llamada Yalk para hacer una publicación local de esto.
Cuando estés listo para convertir el código real, si existen definiciones de tipo existentes, como DefinitelyTyped, te sugiero que las utilices como punto de partida. Lo hicimos para React Redux. Elige algunos archivos, conviértelos a TypeScript y repite. También recomiendo hacer algunos commits que simplemente cambien el nombre de los archivos antes de comenzar a realizar cambios en ellos para preservar el historial de Git existente. Además, no olvides convertir todas tus pruebas a TypeScript. Esto ayudará a detectar algunos problemas, y veremos algunos ejemplos más adelante. Por último, asegúrate de exportar tipos desde tu archivo de índice, no solo las funciones y las estructuras de datos en el código.
Entonces, ¿cómo gestionas la versión de los tipos públicos? Esto es un poco complicado. TypeScript en sí no utiliza la versión semántica. Simplemente utilizan un enfoque de incremento decimal, lo que significa que cualquier nueva versión de TypeScript podría romper el código que se está ejecutando actualmente. Además, diferentes personas pueden tener diferentes configuraciones de TypeScript, lo que puede llevar a diferencias en el comportamiento. Una de las cosas más importantes que vemos es que las personas desactivan las banderas de verificación estricta o de verificación estricta de nulos, lo que conduce a un comportamiento de compilación claramente diferente. En realidad, cualquier cambio en tus tipos, incluidas las correcciones de errores, podría hacer que las aplicaciones de los usuarios no se puedan compilar.
Entonces, ¿eso significa que cada vez que lanzo una corrección de errores, en realidad es una especie de versión importante? El equipo de Ember ha estado trabajando en tratar de establecer un conjunto de reglas sobre cómo planean manejar TypeScript en el ecosistema de Ember, y publicaron un RFC increíblemente extenso donde intentaron definir lo que Sember significa para los tipos de TypeScript, no solo para Ember, sino también como un enfoque que cualquier biblioteca puede utilizar. Y lo que concluyeron es que los tipos son APIs. Debes tener en cuenta los tipos en tu versión, pero puedes dividirlos en cambios que se consideran rompedores y cambios que no se consideran rompedores. Y hacen algunas excepciones para correcciones de errores e intenciones. Es posible que realicen un cambio que introduzca algunos errores en el código de los usuarios, pero que en realidad esté evitando el uso incorrecto de ciertos patrones en el código del usuario. Pero su objetivo es que si sigues la lista de versiones de TypeScript admitidas, no deberías ver nuevos errores en el código de los usuarios. Otro problema relacionado es cómo apuntar a versiones específicas de TypeScript. TypeScript lanza varias versiones nuevas al año y estas pueden tener nuevas características y funcionalidades bastante importantes. Entonces, ¿cuánto tiempo admites versiones antiguas de TypeScript? ¿Qué tan pronto puedes adoptar nueva sintaxis y características? ¿Cómo abordas las pruebas con múltiples versiones de TypeScript? Vale la pena señalar que el repositorio Definitely Typed intenta admitir aproximadamente 2 años de versiones de TypeScript. Diferentes bibliotecas abordan esto de diferentes maneras. Ember ha definido su conjunto de planes de adopción donde han dicho que las bibliotecas principales de Ember utilizarán una ventana móvil donde intentarán admitir nuevas versiones de TypeScript de inmediato y dejarán de admitir versiones antiguas en las versiones de soporte a largo plazo de Ember. Otras bibliotecas de Ember pueden elegir solo un par de versiones. Y cada vez que eliminan una versión, se considera un lanzamiento importante de esa biblioteca.
4. Supporting Multiple TypeScript Versions
Para admitir múltiples versiones de TypeScript, configura tu CI para compilar y probar tu aplicación con diferentes versiones de TypeScript. Durante el desarrollo, utiliza una versión anterior de TypeScript para restringirte a la sintaxis compatible con esa versión.
Por otro lado, el grupo Stately ha dicho que nos reservamos el derecho de realizar cambios en nuestro soporte de TypeScript a medida que nuestra comprensión de TypeScript y su comportamiento evolucione con el tiempo. Entonces, ¿cómo puedes admitir múltiples versiones de TypeScript? Lo más importante que veo aquí es configurar tu CI para compilar tus pruebas y tu aplicación y probarlas con múltiples versiones de TypeScript al mismo tiempo. Esto se puede hacer utilizando algo como el soporte de Matriz en GitHub Actions. También puede ser útil utilizar una versión anterior de TypeScript mientras desarrollas la biblioteca, ya que esto te obliga a restringirte a la sintaxis que
5. Handling Multiple TypeScript Versions
TypeScript tiene soporte incorporado para usar múltiples copias de definiciones de tipos basadas en la versión de TypeScript del usuario. Se recomienda admitir múltiples versiones de TypeScript en tus tipos principales. Las bibliotecas 3DX hacen un esfuerzo por admitir múltiples versiones, teniendo en cuenta la intención al realizar cambios en los tipos. El número de versiones de TypeScript admitidas depende de las características necesarias, generalmente las últimas cuatro o cinco versiones. La documentación de las versiones admitidas podría mejorarse.
6. Debugging Types and Designing APIs
Los tipos de TypeScript pueden tener errores, por lo que los usuarios deben proporcionar reproducciones de problemas. Diferentes configuraciones y entornos pueden causar variaciones en el comportamiento de los tipos, especialmente cuando se desactivan las banderas de estricto o nulo. Depurar tipos no es fácil, pero puedes ajustar la configuración de VS Code y usar el tipo Any.compute en la biblioteca TS Tool Build. Probar los tipos en tu biblioteca es crucial, y las utilidades en las bibliotecas Redux pueden ayudar con esto. TypeScript promueve diseños de API más simples, como se ve en la API de Hooks de React Redux.
Entonces, dado que los tipos son código, eso significa que tienen errores, y los usuarios informarán muchos problemas con los tipos de TypeScript. Y eso significa que necesitamos que los usuarios proporcionen reproducciones de esos problemas, y esto es especialmente cierto porque diferentes usuarios pueden tener diferentes configuraciones y entornos. Como mínimo, necesitas saber qué versión de TypeScript estás utilizando y cómo lo tienen configurado, y esto realmente significa que los usuarios deben proporcionar ejemplos o repositorios de GitHub o enlaces de TypeScript Playground que muestren este tipo de error. Uno de los mayores problemas que vemos es cuando las personas desactivan las banderas de estricto o nulo, y esto realmente introduce diferencias en cómo se comportan nuestros tipos. En este punto, les decimos a los usuarios que si lo desactivan, ese es su problema, no el nuestro.
Desafortunadamente, realmente no hay una forma fácil de depurar tipos. No puedes poner un punto de interrupción en medio de una definición de tipo y detenerte a ver qué está calculando TypeScript en ese momento. Además, si pasas el cursor sobre las variables en VS Code, muchas veces limita el tamaño de la salida de forma predeterminada. Hay un par de formas en las que puedes ajustar esto. Puedes cambiar la bandera de truncamiento de errores en tu archivo de configuración de TypeScript, o puedes sumergirte en el código de TypeScript y modificar el valor codificado para la cantidad de salida que muestra al pasar el cursor en VS Code. Una sugerencia es que si tienes tipos complejos, los recrees paso a paso y los asignes a variables de tipos separadas para ver qué está haciendo en cada paso del proceso. También hay un tipo en la biblioteca TS Tool Build llamado Any.compute que puede realizar una expansión recursiva de tipos. Es muy importante que pruebes los tipos en tu biblioteca, y tenemos archivos de prueba de tipos en las bibliotecas Redux. Y esto es simplemente código de TypeScript que debe compilarse correctamente y tener algunas afirmaciones que indiquen que esperas que esta variable sea de un cierto tipo. Puedes escribir estos archivos de prueba como archivos de prueba normales que se ejecutan a través de TSC. Puedes tenerlos como pruebas omitidas en un conjunto de pruebas. Pero la idea es saber si este código se compila correctamente. Hemos creado muchas utilidades en las bibliotecas Redux para cosas como esperar que ciertos tipos existan. Entonces, cuando se trata de diseñar API, el problema es que JavaScript es un lenguaje muy dinámico. Y TypeScript intenta capturar ese comportamiento y no siempre funciona muy bien. En general, TypeScript nos empuja a diseñar API mucho más simples. Un buen ejemplo de esto es React Redux. La API Connect original es muy dinámica. Toma cuatro parámetros diferentes, todos muy opcionales, algunos de los cuales pueden ser objetos, funciones o nulos. Es increíblemente complicada y las diferencias de tipo para esto son francamente extrañas. Y nuestra API de Hooks es mucho más simple. UseSelector es simplemente una función que toma una entrada conocida y devuelve un resultado. Es más fácil de escribir. Es más fácil de usar. Intentamos diseñar las API de React Redux y Redux Toolkit para que puedan inferir la mayor cantidad posible de información. Queremos que nuestros usuarios tengan que proporcionar la menor cantidad posible de información para obtener
7. Advanced Redux Type Tricks
Nuestros tipos pueden ser complicados, pero facilitan la vida de los usuarios. Proporcionar tipos predefinidos puede ser útil. Las bibliotecas de Redux utilizan tipos condicionales y comparaciones de x extends y. Las ternarias anidadas pueden ser difíciles de leer, pero a veces son necesarias. Redux Toolkit tiene tipos para diferentes escenarios, incluyendo creadores de acciones y createAsyncThunk. En createAsyncThunk, se proporcionan valores predeterminados para tipos opcionales. Reselect tiene un tipo complejo que maneja entradas variádicas. Los genéricos opcionales o con nombre ayudarían a los mantenedores en el futuro.
El comportamiento correcto del tipo. Eso significa que nuestros tipos son más complicados como resultado, pero facilita la vida de nuestros usuarios. A veces no puedes saber realmente los tipos finales que un usuario va a necesitar proporcionar en la función principal o en las definiciones de tipo. Por lo tanto, a veces es necesario proporcionar un tipo que un usuario pueda importar, anular y pasar valores como el estado final de Redux en su aplicación. Tener estos tipos predefinidos que pueden usar con su código puede ser muy útil.
Entonces veamos algunos consejos y trucos de las diversas bibliotecas de Redux. Verás muchos tipos condicionales y los tipos condicionales son básicamente comparaciones a nivel de tipos y operadores ternarios. Y verás muchos x extends y, que es tanto una comparación como una coincidencia de tipo. Al igual que en el código real, puedes anidar estas declaraciones ternarias. Un buen ejemplo de esto es el middleware de thunk para el tipo de Configure Store de Redux Toolkit, donde intentamos determinar qué tipo se debe incluir para el middleware de thunk en la configuración de la tienda en función de las opciones que el usuario ha proporcionado. Entonces, si dijeron que el middleware de thunk debería estar desactivado, entonces no queremos incluir un tipo para el middleware en absoluto. Si incluyeron un valor de argumento adicional, necesitamos usar eso. De lo contrario, se utiliza el tipo predeterminado. O el tipo de acción de carga útil, donde sabemos que queremos comenzar con un campo de carga útil y un campo de tipo, pero también podría haber un campo de meta o un campo de error, dependiendo de cómo se haya definido el creador de acciones. Ahora, es cierto que las ternarias pueden ser difíciles de leer. Personalmente, nunca me han gustado las ternarias anidadas. Y desafortunadamente, en TypeScript, a veces tienes que hacer lo que tienes que hacer. Y sí, esto puede volverse un poco complicado. Este tipo representa todas las formas posibles en que se podría definir un creador de acciones de Redux Toolkit con todos los diversos campos opcionales y que se pueden pasar. De acuerdo. Sí. A veces esto realmente se vuelve ridículo. Como este tipo que representa un creador de acciones createAsyncThunk. Si crees que esto es realmente difícil de leer, sí, estoy completamente de acuerdo. Ahora, un truco que usamos en createAsyncThunk es proporcionar algunos valores predeterminados para varios tipos opcionales y dar a los usuarios una forma de anularlos. Idealmente, todo lo que hacen es proporcionar un tipo para el argumento de entrada y el tipo de retorno. Todo se infiere a partir de ahí. Si necesitas anular algo como el valor del estado para usar con getState, entonces puedes anular condicionalmente solo eso y dejar los otros campos como dispatch y extra sin cambios. En reselect, tenemos un tipo increíblemente complejo que realiza una serie de mapeos y extracciones a nivel de tipos. Me llevó semanas crear este tipo y tuve que recibir mucha ayuda de otras personas, pero esto nos ahorró más de 3000 líneas de tipos definidos múltiples para manejar de 1 a 12 entradas variádicas. Esto requirió muchos trucos, como usar valores predeterminados para los genéricos para precalcular algunas variables o mapear sobre una tupla y asegurarse de que solo usemos campos numéricos. Entonces, ¿qué tipo de cosas ayudarían a los mantenedores en el futuro? Sin duda, lo más importante es
8. Mantener una Biblioteca de TypeScript
Especificar uno o dos genéricos en lugar de todos a la vez nos facilitaría la vida. Sería útil contar con mejores formas de depurar o visualizar tipos y especificar mensajes de error para los tipos. El compilador de TypeScript necesita mostrar mejores mensajes de error. Los miembros del equipo de TypeScript han propuesto formas de mejorar esto.
9. Discusión sobre los Resultados de la Encuesta
Los resultados de la encuesta muestran que un número significativo de encuestados en la conferencia de TypeScript utilizan TypeScript entre el 51 y el 90 por ciento, mientras que el 30 por ciento lo utiliza el 100 por ciento del tiempo. Sorprendentemente, el 14 por ciento de los encuestados no utiliza TypeScript en absoluto, lo que indica un deseo de aprender. Estos resultados reflejan la naturaleza transicional de las aplicaciones, con muchos desarrolladores migrando gradualmente sus bases de código. Como alguien que tiene experiencia en esto, encuentro los resultados esperados y comprensibles.
10. Upgrading TypeScript and Verifying Functionality
Si actualizar la versión de TypeScript es un proyecto que no es una biblioteca, deberías poder actualizar TypeScript y posiblemente reiniciar VS code para asegurarte de que el servidor de lenguaje esté analizando correctamente las cosas. También es importante volver a ejecutar un paso de compilación y compilar todo el proyecto para verificar su funcionalidad.