Inicio / Artículos / Qué hace y qué no hace el soporte nativo de TypeScript en Node.js

Qué hace y qué no hace el soporte nativo de TypeScript en Node.js

Este artículo explica cómo Node.js ejecuta los archivos .ts de forma nativa mediante la eliminación de tipos, por qué omite la verificación de tipos y cuándo aún se necesita un paso real de compilación.

1595 palabras

Ya conoces el procedimiento. Creas un nuevo proyecto en TypeScript, escribes tu primer archivo .ts, intentas ejecutarlo y de inmediato recuerdas que hay todo un ritual de configuración que debes completar primero: instalar ts-node o tsx, configurar un tsconfig.json, quizás establecer una secuencia de compilación y decidir si vas a usar CommonJS o ESM. Nada de eso es especialmente difícil por sí solo. Es simplemente fricción que se acumula antes de que escribas cualquier lógica real de la aplicación, y ocurre cada vez que inicias algo nuevo.

Este año, en una parte considerable de los proyectos reales con Node.js, todo ese ritual simplemente ha desaparecido. Escribir node file.ts ya funciona directamente. Sin flags, sin dependencias adicionales, sin archivos de configuración. El cambio se implementó en silencio, sin ningún anuncio importante, pero se trata del tipo de eliminación de pequeñas complicaciones que normalmente uno enfrentaría repetidamente a lo largo de la semana; y eso suma algo realmente digno de explorar.

¿Qué está sucediendo realmente?

El mecanismo subyacente se denomina type stripping, y el nombre es sorprendentemente literal: Node.js analiza tu código fuente en TypeScript, elimina las anotaciones de tipo y ejecuta el JavaScript puro que queda. Ese es el concepto completo.

// Before: what you write
interface User {
  name: string;
  age: number;
}
function describeUser(user: User): string {
  return `${user.name} is ${user.age} years old`;
}
// After: what Node.js actually executes, post-stripping
// (whitespace preserved, so line numbers stay accurate for debugging)
function describeUser(user) {
  return `${user.name} is ${user.age} years old`;
}

La declaración de la interface desaparece por completo. Las anotaciones como : User y : string también se eliminan. Lo que queda es un JavaScript ordinario y válido que V8 ejecuta tal como siempre lo ha hecho; no hay ningún mecanismo especial en tiempo de ejecución, ni polyfills, y conceptualmente no ocurre nada nuevo durante la ejecución.

Detrás de escena, este proceso se lleva a cabo mediante una biblioteca llamada Amaro, que es un envoltorio ligero alrededor de @swc/wasm-typescript, una compilación en WebAssembly del analizador de TypeScript que SWC desarrolló en Rust. La velocidad no proviene de alguna optimización ingeniosa, sino del hecho de realizar una cantidad considerablemente menor de trabajo que un compilador completo. No resuelve tipos entre varios archivos, no verifica si las anotaciones son correctas y no genera archivos de declaración. Simplemente analiza el árbol sintáctico, elimina las partes propias de TypeScript y devuelve JavaScript. Ese alcance reducido es precisamente la razón por la que es rápido.

El soporte de Node para esta función pasó por varias fases antes de alcanzar su forma actual: el soporte experimental para la eliminación directa de tipos se incluyó en v22.6.0, apareció una bandera separada para manejar estructuras más complejas como los enums en v22.7.0, y toda la función se volvió estable por defecto tanto en v22.18.0 como en v24.3.0 — lo que significa que no se necesitan banderas en absoluto para el código que sigue la sintaxis soportada. Cabe destacar que Node eliminó posteriormente esa bandera específica para enums, optando por mantener un alcance deliberadamente limitado y predecible en lugar de intentar soportar todo el lenguaje.

Los límites reales

Esta es la parte crucial que hay que entender si va a depender de esta función, y merece ser expresada claramente: la eliminación de tipos no es lo mismo que la verificación de tipos.

Eliminar una anotación de tipo no confirma primero que sea correcta, sino que simplemente la elimina. Por lo tanto, un archivo que contenga un error de tipo real, por ejemplo, al pasar una cadena donde se esperaba un número, funcionará sin problemas con la eliminación de tipos, ya que para cuando el código se ejecute realmente, la información de tipo que habría detectado el problema ya ha desaparecido. Todas las fuentes serias sobre este tema coinciden en el mismo consejo: continúe ejecutando tsc --noEmit como paso independiente en su pipeline de CI. La eliminación de tipos sustituye al paso de compilación, no a la función del compilador de detectar errores realmente.

La restricción más importante se refiere a determinar con exactitud qué sintaxis de TypeScript es apta para ser eliminada. Node solo admite lo que se conoce como sintaxis eliminable: constructos del lenguaje que pueden quitarse por completo sin alterar el funcionamiento del código al ejecutarse. Una parte significativa de TypeScript no cumple con este requisito, ya que genera un comportamiento real en tiempo de ejecución que no puede simplemente eliminarse:

// ❌ Fails under type stripping — enums generate a real runtime object
enum Direction {
  Up,
  Down,
  Left,
  Right,
}
// ❌ Fails - parameter properties generate constructor assignment code
class Point {
  constructor(public x: number, public y: number) {}
}
// ❌ Fails - this is a CommonJS-style module alias, not an erasable type
import fs = require('fs');
// ❌ Fails - angle-bracket type assertions look like real syntax to strip,
// but the parser can't tell it apart from JSX safely
const num = <number>someValue;

Cada una de estas construcciones provoca un fallo grave en lugar de una compilación incorrecta que pase desapercibida; el mecanismo de eliminación de Node está diseñado intencionadamente para detenerse y mostrar un error en lugar de intentar adivinar lo que se pretendía. Los decoradores de estilo antiguo, habilitados mediante la antigua bandera experimentalDecorators, se enfrentan al mismo problema por la misma razón. Los decoradores más recientes, basados en estándares y desarrollados por TC39, son otra historia: están especificados de tal manera que se compilan a sintaxis normal de JavaScript, por lo que no queda nada especial que eliminar, y funcionan sin problemas en Node.

Cómo se adaptó TypeScript

En lugar de dejar que los desarrolladores descubran estos límites uno por uno mientras ejecutan el código, el equipo de TypeScript actuó rápidamente para hacer explícitas las reglas. TypeScript 5.8 incluyó una nueva bandera del compilador, --erasableSyntaxOnly, que hace que tsc rechace por sí mismo cualquier patrón no eliminable descrito anteriormente durante la compilación. Esto transforma la pregunta “¿Node realmente ejecutará esto?” de algo que se descubre a costa de errores en tiempo de ejecución a una regla que se puede aplicar desde el principio, como una restricción explícita para todo el código.

// tsconfig.json
{
  "compilerOptions": {
    "erasableSyntaxOnly": true,
    "verbatimModuleSyntax": true  // pairs well with this —
                                   // keeps type-only imports explicit
  }
}

Vale la pena activar esta opción incluso si aún no tienes planes de eliminar el paso de compilación, simplemente porque te brinda una respuesta definitiva y automatizada sobre si tu código cumple los requisitos, en lugar de averiguarlo poco a poco a medida que surgen problemas.

La cantidad de trabajo de migración que esto implica varía mucho según el punto de partida. Un servicio backend o una herramienta de línea de comandos recién creados suelen poder activar inmediatamente erasableSyntaxOnly con poco o ningún ajuste necesario. En cambio, un código que depende en gran medida de declaraciones enum, o uno desarrollado con un framework que asume decoradores antiguos —como las configuraciones más viejas de NestJS o TypeORM— requiere un esfuerzo real de reescritura, o bien una decisión consciente de mantener un pipeline de construcción tradicional en lugar de migrar todo de una sola vez. La forma más fiable de evaluar el trabajo antes de comprometerse a algo es activar erasableSyntaxOnly, ejecutar tsc --noEmit una vez y observar cuántos errores aparecen. Esa sola ejecución le mostrará el verdadero alcance del problema antes de modificar cualquier configuración en tiempo de ejecución.

Guía práctica

En los equipos que ya han pasado por esta transición, ha surgido un marco de toma de decisiones bastante consistente.

Omita el paso de compilación para los servicios backend, las herramientas de línea de comandos, las utilidades internas y los scripts independientes: cualquier cosa que se ejecute directamente bajo Node y que no se publique como un paquete para que otros lo utilicen. Este es exactamente el tipo de caso para el cual se creó la función de eliminación de componentes, y se informa comúnmente que los servicios desarrollados con Express o Fastify funcionan de inmediato con ella, sin necesidad de realizar cambios en el código.

Mantenga un paso de compilación para todo lo que se ejecuta en el navegador, ya que estos no pueden ejecutar archivos .ts en absoluto; seguirá necesitando un bundler independientemente de lo que soporte Node en el servidor. También incluya un paso de compilación para cualquier paquete npm que publique, porque quienes lo instalen necesitarán JavaScript compilado además de archivos de declaración .d.ts, y no tiene garantía de que su versión de Node incluso soporte la eliminación de tipos. Además, mantenga un paso de compilación para cualquier código que aún dependa de decoradores obsoletos o de un uso intensivo de enum que aún no haya sido convertido.

Cualquiera que sea el camino que elija, siga ejecutando tsc --noEmit en CI. Eliminar el paso de compilación solo quita la fase de compilación en sí; nunca estuvo destinado a eliminar la verificación de tipos, y tratarlo como un sustituto de tsc es la única forma real en que este cambio podría costarle silenciosamente seguridad.

La conclusión real

Lo que hace interesante este cambio no es principalmente la ganancia de velocidad, aunque un ciclo de retroalimentación más rápido representa una ventaja real e inmediata. Se trata de una señal sobre la dirección conceptual que está tomando TypeScript. Durante casi toda su historia, TypeScript ha sido descrito como un lenguaje que se compila a JavaScript: un lenguaje distinto, traducido antes de poder ejecutarse. La eliminación silenciosa de tipos en Node sugiere que TypeScript está evolucionando hacia ser tratado más bien como una variante de JavaScript que un entorno en tiempo de ejecución puede leer tal cual, al menos para el subconjunto cotidiano y ampliamente utilizado del lenguaje por parte de la mayoría de los desarrolladores. Eso no representa todo el lenguaje, y nunca lo hará: las enumeraciones y los decoradores heredados siguen teniendo casos de uso reales y no desaparecerán. Pero para todo el código que no los necesita, el paso que antes existía entre escribir TypeScript y ejecutarlo ya no es obligatorio.

Eso representa un cambio mucho más significativo de lo que sugeriría la escasa atención que recibió.

Lecturas relacionadas