3 puntos por GN⁺ 2024-07-26 | 1 comentarios | Compartir por WhatsApp
  • Se fusionó el PR #53725 de Node.js, lo que habilita soporte experimental para ejecutar archivos TypeScript con la bandera --experimental-strip-types
  • Esta función elimina la sintaxis de tipos del código fuente TypeScript para convertirlo en código JavaScript, y no realiza verificación de tipos durante la conversión
  • La implementación inicial no transforma funciones exclusivas de TypeScript como Enum y namespaces, por lo que se limita a código que se vuelve JavaScript solo con quitar los tipos
  • Para la implementación se eligió @swc/wasm-typescript por su simplicidad, ya que no hace falta agregar Rust ni Go al toolchain de Node.js
  • En la discusión se trataron como restricciones principales la regla de extensiones de archivo obligatorias en ESM, la práctica común en TypeScript de que un import con .js apunte en realidad a un archivo .ts, y el costo de los source maps y de las ubicaciones en el stack de errores

Cómo funciona --experimental-strip-types

  • Al habilitar --experimental-strip-types, Node.js puede ejecutar archivos TypeScript
  • Node.js transpila el código fuente TypeScript a código fuente JavaScript
  • Durante la transpilación no se hace verificación de tipos, y la información de tipos se descarta
  • El ejemplo es el siguiente
const foo: string = "foo";
const foo = "foo";

Por qué se eligió solo eliminar tipos y cuáles son las limitaciones iniciales

  • El punto central de este PR es la eliminación de sintaxis de tipos dentro de la gramática de TypeScript
  • Otros runtimes también convierten a JavaScript funciones exclusivas de TypeScript que no existen en JavaScript, como enum
  • La implementación inicial de este PR no realiza ese tipo de transformaciones
    • No se puede usar Enum
    • No se puede usar namespaces
    • Otras funciones exclusivas de TypeScript tampoco son objetivo de conversión
  • Se incluye una indicación de consultar typescript.md dentro de los cambios del PR para más detalles y limitaciones

Se eligió @swc/wasm-typescript como herramienta de implementación

  • @swc/wasm-typescript es un paquete pequeño, compuesto por un archivo wasm y un archivo JavaScript que lo enlaza
  • Otras herramientas requerían agregar Rust o Go al toolchain de Node.js
  • SWC también se usa en Deno para el mismo propósito, y el autor del PR lo considera una opción ya probada
  • A futuro, se considera que esta función podría implementarse en una capa nativa
  • Este PR hace referencia a nodejs/loaders#217

Discusión sobre resolución de imports y extensiones

  • No se admiten imports sin extensión como import foo from "./foo"
  • En el ESM de Node.js existe la regla de mandatory file extensions, por lo que no admitir imports sin extensión se trata como un comportamiento intencional
  • Se señaló que en codebases con módulos TypeScript es común usar la extensión .js en el import aunque el archivo real sea .ts
    • Ejemplo: import foo from './foo.js'
    • En el sistema de archivos puede no existir ./foo.js y existir solo ./foo.ts
  • El autor del PR considera que este problema puede abordarse en iteraciones futuras, y que adivinar extensiones requeriría verificaciones adicionales del sistema de archivos y heurísticas
    • Si ./foo.js no existe, revisar ./foo.ts
    • Si ./foo.ts no existe, revisar ./foo.js
  • Esta discusión se sigue en nodejs/loaders#208

Ubicaciones del código fuente, source maps y stack de errores

  • En la revisión surgió la opinión de que en el corto plazo se necesitan ubicaciones de origen precisas en el stack de errores
  • Una forma posible es no cambiar las ubicaciones del código fuente durante la eliminación de tipos, para así evitar la generación y el mapeo de source maps
  • Se mencionó que los source maps tienen costos en uso de memoria y rendimiento, y que no son algo simple
  • El autor del PR considera que, como SWC ya soporta source maps, se podría agregar una bandera para activarlos
  • También cree que podría pedirse a SWC una opción para reemplazar la sintaxis eliminada por espacios en blanco, evitando así cambiar el stack de errores original

Temas de diseño pendientes antes de estabilizarlo

  • El soporte para TypeScript se divide en dos ejes
    • Eliminación de sintaxis de tipos
    • Ajuste de las reglas de resolución para aceptar archivos .ts
  • Surgió la preocupación de que cambiar las reglas de resolución pueda entrar en conflicto con los loaders TypeScript existentes
  • El autor del PR respondió que este PR no incluye override de reglas de resolución y que la extensión del archivo sigue siendo necesaria
  • Se planteó que allowImportingTsExtensions puede permitir la forma import './file.ts', y que este enfoque también encaja con Deno
  • Para ejecutar directamente el mismo archivo fuente y al mismo tiempo generar salida JavaScript con una herramienta de build, se considera que la responsabilidad de convertir file.ts en file.js puede quedar del lado de la herramienta de build

Fusión y trabajo posterior

  • El PR se fusionó en nodejs:main el 24 de julio de 2024
  • El cambio abarca 8 commits, 89 archivos, 2,190 líneas agregadas y 25 líneas eliminadas
  • Lleva etiquetas como semver-minor, experimental, module, esm y notable-change
  • Hubo discusión sobre si mencionar esta función en la página de guía de TypeScript del sitio web, y también se vinculó un PR de actualización relacionado
  • Como tema posterior, también se discute incluir en los metadatos de distribución la versión de TypeScript que usa el binario de Node para eliminar tipos

1 comentarios

 
GN⁺ 2024-07-26
Opiniones de Hacker News
  • La eliminación de tipos de TypeScript es imposible sin la sintaxis de TypeScript. La eliminación de tipos no es una operación a nivel de tokens, y la sintaxis de TypeScript sigue cambiando.
    Por ejemplo, en TypeScript 1.5, foo < bar & baz > ( x ) se parseaba como (foo (x)) porque bar&baz no era una expresión de tipo válida, pero después de que se agregó el operador de tipos de intersección, se parsea como foo<(bar & baz)>(x) y se desambigua como foo(x).
    Para seguir usando las funciones más recientes de TypeScript, hay que compilar a JS o mantener siempre actualizada la versión de Node, lo que puede ser un compromiso difícil de aceptar para equipos que prefieren quedarse en Node LTS.

    • Parece que el equipo de Node ya consideró este problema. Como existe el precedente de que Node incluye npm pero permite actualizarlo por separado, se está discutiendo que el transformador de TypeScript podría manejarse de forma similar.
      La idea sería dejar una versión estable en el core, pero si TypeScript lanza nuevas funciones o cambios incompatibles, o si los usuarios quieren usar nuevas funciones experimentales, permitir una actualización separada desde NPM.
      Así se podría dar soporte a una versión específica de TypeScript durante los 3 años de vida de una versión de Node.js, sin dejar a los usuarios atados a ella: https://github.com/nodejs/loaders/issues/217
    • Desde el punto de vista de la eliminación de tipos, hubo muchas más versiones de TypeScript en las que la sintaxis fue estable que períodos en los que fue inestable. Si se retrocede hasta la 1.5 es porque desde la serie 2.x ha sido bastante estable.
      El último gran cambio sintáctico probablemente fueron los tipos condicionales de la 2.8, cuando se introdujo una sintaxis parecida a un operador ternario if en posiciones de tipo. Si se quiere hacer también verificación de tipos, el modelo de tipos ha cambiado mucho desde la 2.x, pero la sintaxis en general se mantuvo estable, y la mayor parte de la innovación de TypeScript estuvo en el modelo de tipos y la inferencia de tipos.
      Aunque todavía está en una fase temprana, Stage 1, también hay un intento de estandarizar buena parte de la sintaxis de tipos de TypeScript con el objetivo de eliminar tipos, no de verificarlos: https://github.com/tc39/proposal-type-annotations
    • También habría que actualizar el compilador. Al final se puede expresar como “no se puede subir la versión de TypeScript de forma independiente de la versión de Node.js”, pero probablemente no sea un gran problema.
    • Usar los signos de desigualdad como corchetes angulares realmente parece haber sido un error.
    • Los equipos que siempre siguen la versión más reciente de TypeScript y usan nuevas funciones del lenguaje probablemente también sigan la versión más reciente de Node. Según mi experiencia, las actualizaciones de TypeScript requirieron migraciones o correcciones con más frecuencia que las de Node, debido a nuevos errores.
      Los equipos que no tienen demasiado interés en las funciones más recientes de V8 o Node y se quedan en LTS probablemente tampoco estén tan interesados en las funciones más recientes de TypeScript.
  • Si Node.js puede ejecutar archivos TypeScript directamente, el compilador de TypeScript podría usarse solo como verificador de tipos, en lugar de para eliminar tipos y transformar JavaScript. Es parecido a cómo en Python los verificadores de tipos revisan los tipos y los dejan tal cual, mientras el intérprete ignora las anotaciones de tipo.
    Curiosamente, en Python surgieron varios verificadores de tipos populares que usan la misma sintaxis de anotaciones de tipo, pero con semánticas distintas. En cambio, en JavaScript parece que TypeScript se convirtió prácticamente en el único verificador de tipos popular.
    También he oído de casos en Python donde no se verifican los tipos y solo se escriben en el código fuente como una sintaxis de comentarios más cómoda. Si Node.js admite ignorar tipos, ese enfoque también sería posible en JavaScript.

    • Flow de Facebook fue bastante importante en JavaScript hace algunos años, pero ahora parece que TypeScript ganó de forma bastante clara.
    • Aunque no se haga verificación de tipos, el IDE puede usar las anotaciones de tipo para mejorar el autocompletado y cosas similares, así que agregar tipos en algunos lugares sigue teniendo sentido.
    • Incluso hoy se puede hacer sin compilación agregando tipos con JSDoc y validándolos con TypeScript. Las builds se vuelven más rápidas y se obtiene código que funciona en todas partes sin procesamiento mágico ni eliminación de nada salvo comentarios.
      Personalmente, la mayor molestia de usar JSDoc era la sintaxis de importación, pero eso cambió desde TypeScript 5.5 y ya no es un problema.
    • Node.js no es el único runtime de JavaScript. Hasta que todos los navegadores puedan ejecutar TypeScript directamente, hay que compilar TS a JS para el navegador.
      Dicho eso, algunos bundlers ya hacen esto usando compiladores no oficiales como SWC, y Node también está probando SWC para esta función.
      Las anotaciones de tipo no son solo comentarios simples; como los nombres, también son pistas para que el IDE muestre un mejor autocompletado.
    • Hay una propuesta de EcmaScript en esta dirección: https://github.com/tc39/proposal-type-annotations
      Creo que esto debería formar parte de la especificación del lenguaje.
  • Me da curiosidad cómo reaccionará el ecosistema de NPM si esta función deja de estar detrás de una bandera y pasa a ser el valor predeterminado. No sé si los contribuidores seguirán compilando versiones CJS y ESM al publicar módulos de NPM, o si simplemente pondrán engine: nodejs >= 25 en package.json y omitirán el paso de compilación.
    Personalmente, me gustaría más que los módulos de NPM que tienen fuentes en TS dejaran de distribuir dist/.cjs, porque así quedaría más claro dónde poner el depurador y los console.log. Desde el punto de vista de quienes contribuyen a NPM, la tentación de eliminar el paso de compilación también será grande.
    Pero si una sola dependencia distribuye archivos TS, la premisa de que “todos reciben archivos TS” podría propagarse rápidamente. En el momento en que Node.js saque esto de una bandera experimental opt-in, parece que toda la comunidad empezará a esperar implícitamente que los consumidores acepten archivos TS.
    Si eso pasa, en unos meses Firefox y Safari también se verán presionados a aceptarlo, y todos los compiladores de JS no tendrán más remedio que descartar las anotaciones de tipo de TS.
    Personalmente, eso tampoco me parece mal. Incluso quienes usamos TS estamos poniendo en los módulos de NPM un paso de transformación que convierte el código TS en JS y .d.ts para dar soporte a algún usuario hipotético de JS. Pero si Node acepta archivos .ts, ese paso de transformación puede eliminarse sin que nadie lo note; me pregunto qué mecanismos impedirán que los publicadores de NPM dejen de distribuir archivos js/d.ts y ni siquiera se den cuenta de que rompieron algo.

    • Ryan Dahl está creando un nuevo registro de paquetes llamado JSR para resolver este problema.
      La idea es que puedas subir código TypeScript sin paso de compilación y que, cuando otros desarrolladores lo instalen, vean el código fuente TypeScript original en lugar de JavaScript transformado.
    • No se admite ejecutar archivos .ts dentro de node_modules. Es una de las principales restricciones para no romper el ecosistema.
    • Las bibliotecas antiguas de TypeScript que se siguen manteniendo se están transformando a .cjs y .mjs para npm, así que probablemente terminen distribuyendo las tres versiones.
      Si fuera un proyecto nuevo escrito desde cero, quizá solo distribuiría TypeScript y no haría la transformación.
      Edición: según un comentario hermano de satanacchio, quien parece ser el autor del PR y miembro del Comité Técnico Directivo de Node.js, el soporte de TS es para elementos de nivel superior y no se aplica a bibliotecas dentro de node_modules.
    • Quiero publicar mi código fuente .ts en npm, pero el equipo de TypeScript se opuso mucho a eso por problemas con tsconfig y de rendimiento. Aun así, tengo expectativas.
  • Tal vez algún día Node permita hacer reflexión en tiempo de ejecución de estos tipos desde JS.
    Eso sería una ganancia enorme. En Python, como se puede reflexionar sobre los tipos, excelentes herramientas como pydantic pueden generar validaciones a partir de los tipos.
    Con una sola notación estándar se puede obtener verificación de tipos, validación de datos en tiempo de ejecución, generación de API y generación de documentación de API.
    Hoy en JS, herramientas como Zod básicamente tienen que reinventar lo que TypeScript ya hace, con cosas como const mySchema = z.string();.

    • z.string() de Zod no solo ofrece seguridad de tipos como TypeScript. TypeScript ofrece verificación de tipos en tiempo de compilación, y Zod agrega validación y parsing en tiempo de ejecución.
      z.string() convierte mySchema en un esquema funcional capaz de parsear y validar. mySchema.parse("some data") tiene éxito, y mySchema.parse(321) lanza una excepción.
      Lo he usado donde hacía falta validación en tiempo de ejecución y comprobaciones dentro del proceso, y funcionó bastante bien; también se pueden extraer tipos con algo como const A = z.string(); type A = z.infer; // string.
      Es decir, si primero defines el tipo en Zod e infieres el tipo desde ahí, obtienes verificación de tipos tanto en tiempo de compilación como en tiempo de ejecución. Puede ser excesivo para bases de código pequeñas que se mueven rápido, pero en situaciones donde se requiere validación dentro del proceso es bastante útil y no implica tanto trabajo.
    • Últimamente el foco del equipo de TypeScript está muy puesto en alinearse con el lenguaje JavaScript, y las funciones nuevas como tipos en tiempo de ejecución quedaron casi descartadas, o al menos relegadas detrás de la propuesta de tipos de JavaScript de TC39. Algo parecido pasó con el uso de decoradores en variables fuera de estructuras de clase.
      Aun así, TypeScript permite plugins. No se usan mucho, pero pueden agregar al lenguaje otras funciones que se transforman al JavaScript resultante.
      Un plugin relacionado con tipos en tiempo de ejecución es Typia, que permite usar firmas de tipos de TypeScript en tiempo de ejecución en guards como assert(myValue). Intercepta esa llamada de función y genera en el JavaScript transformado sentencias if exhaustivas que verifican la naturaleza de la variable pasada.
      No parece que vaya a formar parte del lenguaje en los próximos 4 a 6 años, pero ya existen bibliotecas que permiten hacer esto hoy.
    • Si JS algún día agrega verificación de tipos, espero que no elija TypeScript.
      En realidad hace falta un sistema de tipos sólido, pero TS es deliberadamente no sólido. Se necesita un sistema de tipos que no permita malas prácticas de programación como TS, y que obligue a un diseño que permita crear programas rápidos. Hace falta un sistema de tipos Hindley-Milner.
      Si quieres convertir un módulo en código tipado, bastaría con agregar "use type". Este modo debería prohibir las partes malas, como la coerción de tipos, y bloquear elementos que perjudican el rendimiento, como cambiar la forma de los objetos o los tipos de valores, o crear arreglos mezclando valores arbitrarios.
      Los datos que entren desde módulos sin tipos se transformarían o, si no pueden transformarse, lanzarían un error; entonces el compilador podría optimizar profundamente el código tipado con garantías de tipos más fuertes y sin riesgo de tener que abandonar a mitad de camino.
    • Me pregunto si esto significa que Node podrá saber si una excepción es una subclase de ValueError, o si un objeto es una instancia de SomeClass. Soy principiante en TS, así que pensaba que en JS no había tipos más allá de arreglos, objetos, números y strings, y que Zod o las funciones de type guard devolvían objetos comunes del estilo “confía en mí”.
  • La experiencia de desarrollo de Bun está a un nivel bastante sin precedentes en este ámbito, y ahora la mayoría de los casos de uso están cubiertos, o Bun no se crashea al escribir scripts de ejecución con bun run
    En cambio, Node ni siquiera permite configurarlo para que no exija extensiones en los import, y tampoco se puede hacer que tsc agregue automáticamente la extensión .js al resultado compilado, así que hay que sumar un bundler
    El soporte nativo de TypeScript corregiría bastante esta pequeña molestia, pero incluso cuando se estabilice, cuesta imaginar que iguale a Bun en experiencia de usuario o rendimiento

    • Las extensiones deberían ser obligatorias. No se puede hacer búsqueda de rutas en la red como en un disco local, y los runtimes conectados a la red, como los navegadores, son muy importantes en JavaScript
    • Me gusta mucho Bun, pero Deno sigue siendo más maduro, estable y con más funciones. Tiene cosas como workers estables, HTTP/2, y según el caso de uso V8 puede ser más rápido que JSC
      La experiencia de desarrollo y las herramientas también son de primer nivel. Deno también puede hacer verificación de tipos y, si no recuerdo mal, incluye TSC
      Bun genera mucho ruido, pero para trabajo serio hoy Deno es claramente una mejor opción. Aun así, la visión y la capacidad de ejecución de Bun son impresionantes y son algo bueno para los desarrolladores
    • Probé Bun dos veces con unos meses de diferencia, pero en Windows fallaba al ejecutar bun install, así que nunca pude hacerlo funcionar correctamente: https://github.com/search?q=repo%3Aoven-sh%2Fbun+bun+install...
    • “Sin precedentes” será si dejamos fuera a Deno
    • Siento que Bun todavía está demasiado verde. Está hecho en Zig, que ni siquiera llegó a 1.0
  • Hace tiempo que quería un runtime de TypeScript y también me gusta mucho TypeScript, pero me da gracia que, después de haber dejado Java hace mucho, al final esté buscando algo mucho más parecido a Java
    Parece que, al final, lo que queríamos era Java con JIT, un sistema de tipos más rico y tipado gradual. El ecosistema de npm también tiene muchos defectos, pero usar librerías en este ecosistema es mucho menos pesado y más divertido
    Sorprendentemente, Rust está en el otro extremo del espectro de lenguajes, pero a veces da una sensación parecida
    Edición: la expresión JIT no era adecuada. La usé de forma imprecisa, pero lo que quería decir eran las diferencias de tiempo de arranque y de runtime al ejecutar algo en la JVM y en V8. Java se siente pesado, mientras que el ecosistema de JavaScript se siente mucho más ágil

    • Java fue literalmente quien popularizó el término JIT, así que no sé qué significa “Java con JIT”
      Tampoco sé en absoluto si TypeScript está más cerca de Java. Lo único que comparten es el nombre equivocado de “Javascript” y el soporte para programación imperativa; en todo lo demás me parecen muy distintos
    • En mi experiencia, TypeScript es mucho mejor que Java. Mucho menos verboso y más flexible
    • Personalmente, prefiero mucho más usar TypeScript que Java. En especial si evitás las partes algo incompletas de las clases y te enfocás en interfaces y funciones, la usabilidad es mucho mejor
      Dicho eso, algo que tiene Java y extraño en TypeScript es la reflexión de tipos en runtime. En el ecosistema de TypeScript hay incontables formas de rodearlo, pero todas me parecen un poco feas
    • Java tiene JIT. Y me da curiosidad saber en qué sentido el sistema de tipos de TypeScript tiene más funciones que el de Java
    • Estoy en una posición similar. Personalmente, lo que quiero es una biblioteca estándar y ecosistema de C# sin las partes legacy actuales o prácticamente abandonadas, sumado a la facilidad y potencia de los tipos algebraicos estructurales
      AoT también me parece bien, y estaría bueno tener una opción de binario único. Idealmente, sin runtime y con eliminación inteligente. Si además pudiera correr con JIT en el navegador, mejor
      También quiero comodidades del compilador y del verificador de tipos como pattern matching exhaustivo
  • Una función de Deno que me gusta está llegando directamente a Node. Muy bueno
    Quizá ya no tenga que instalar siempre esbuild para eliminar tipos. Me entusiasma que vaya a ser mucho más fácil escribir y usar scripts en TypeScript
    Últimamente prefería Python para scripts de una sola vez, pero en cuanto a tipos, personalmente considero que TypeScript es mejor que Python. Sobre todo en scripts grandes que volvés a mirar meses después, los tipos aportan mucho

  • Fue un mes realmente movido para Node. Primero se agregó node:sqlite en v22.5.0, y ahora también está llegando el soporte de TypeScript. Me gusta la dirección que está tomando Node

    • Parece ser por la influencia o la competencia de Bun. Es algo bueno para todos
    • El test runner agregado recientemente también está buenísimo
  • Soy el autor del PR. Pueden preguntarme lo que sea

    • Me interesa saber cuál es el siguiente paso y qué futuro creés que puede tener TypeScript en Node y en todo el ecosistema JS
  • Hace mucho empecé a usar Node.js para trabajo de backend, y parecía tener muchas ventajas frente a escribir código en PHP sin arrastrar muchos de los problemas de Java.
    Pero Node era algo tosco, y se sentía como un lenguaje al que había que pegarle varias cosas para convertirlo en el lenguaje que uno quería. Al final empecé a usar Go, que era mucho más fácil de usar y, aunque a veces mucho más verboso, la seguridad de tipos simplificaba la programación.
    TypeScript parecía una buena opción, pero al final era otro añadido más. No tengo claro qué valor aporta usar TypeScript en lugar de Go. Tener tipos claros está bien, pero no resuelve otros problemas del lenguaje que Go ya resolvió. Deno también resuelve algunos.
    La gran ventaja de Node frente a Go es que permite prototipar rápido, pero usar TypeScript parece reducir bastante esa ventaja. Por eso es difícil decidir si esto es un buen avance o si hace que Node pierda las características que lo volvían una buena opción.

    • TypeScript es JS más seguro. Aunque uses TS, sigues usando JS.
      Por la expresión “añadido”, quizá el problema no sea tanto el lenguaje en sí, sino la ausencia de un framework opinado que gestione todo por defecto, como Django. Me gusta usar Django, pero salirse del camino establecido es un poco más difícil.
    • La respuesta obvia es la familiaridad con el lenguaje. Si el código de frontend del proyecto está en JavaScript o TypeScript, usar Node es una elección fácil. Se pueden usar bibliotecas compartidas, tipos compartidos, etc.
    • Puede ser porque ya conoces JavaScript o porque hay experiencia interna con él, lo cual es mejor que aprender Go. Nosotros también teníamos experiencia previa en frontend y lo estamos usando junto con cdktf; parecía una opción más lógica que Go.