Type stripping en Node.js: ejecutar TypeScript sin compilar, explicado
Node.js puede ejecutar archivos .ts directamente desde hace ya un tiempo. No hace type-checking ni soporta todo TypeScript: esto es lo que de verdad ocurre por dentro.
node script.ts funciona. Sin ts-node, sin tsx, sin paso de compilación previo, sin configurar nada en la mayoría de casos. Es una de esas cosas que suenan a magia hasta que entiendes que Node no está “entendiendo TypeScript”: está haciendo algo mucho más simple y mucho más limitado, y esa limitación es precisamente lo que la hace rápida y segura de mantener.
Qué es exactamente el type stripping
Node.js no incorpora un compilador de TypeScript. Lo que hace, cuando encuentra un archivo .ts, es un proceso llamado type stripping: recorre el código, localiza la sintaxis que es exclusivamente de tipos (anotaciones de parámetros, interfaces, genéricos, as Tipo, firmas de función) y la sustituye por espacios en blanco de la misma longitud. No la elimina reescribiendo el archivo: la borra dejando el mismo número de caracteres y de saltos de línea.
function saluda(nombre: string): string {
return `Hola, ${nombre}`;
}
se convierte, en memoria, en algo funcionalmente equivalente a:
function saluda(nombre ) {
return `Hola, ${nombre}`;
}
Ese detalle —reemplazar por espacios en vez de reescribir— es la razón por la que el proceso no necesita generar source maps: los números de línea y columna del código resultante coinciden exactamente con los del archivo original, así que una traza de error apunta directamente a la línea correcta del .ts sin ninguna traducción adicional.
graph LR
A[archivo .ts] --> B{Type stripping}
B -->|sintaxis de tipos| C[sustituida por espacios]
B -->|sintaxis JS válida| D[se mantiene igual]
C --> E[código JS equivalente en memoria]
D --> E
E --> F[V8 ejecuta directamente] Desde qué versión funciona sin flags
El soporte llegó por fases, y merece la pena conocerlas porque en producción es habitual encontrarse versiones LTS distintas conviviendo:
- Node 22.6 introdujo
--experimental-strip-typescomo flag experimental. - Node 22.18 activó el borrado de tipos sin necesidad de ningún flag, siempre que el código use solo sintaxis “erasable” (borrable sin generar código nuevo).
- A partir de ahí, las versiones posteriores de la línea 22 y las líneas 23/24 lo llevan activado por defecto; una LTS reciente como la 24 lo trae de fábrica, y versiones más nuevas han ido estabilizando el mecanismo y retirando progresivamente los flags experimentales asociados a la fase inicial.
Lo que NO puede hacer, y por qué es una limitación intencionada
Aquí está la parte que genera más confusión, y conviene entenderla bien porque cambia cómo se escribe el código para este entorno.
No hace comprobación de tipos
Node borra la sintaxis de tipos; no la valida contra nada. Si escribes function suma(a: number, b: number): string, Node lo ejecuta sin rechistar aunque el tipo de retorno declarado sea absurdo. La comprobación de tipos sigue siendo trabajo de tu editor (vía tsserver) y de tu pipeline de CI ejecutando tsc --noEmit. El type stripping resuelve “cómo ejecuto esto rápido”, no “cómo sé que es correcto”.
No soporta sintaxis que genera código nuevo
El borrado de tipos solo puede eliminar código; no puede transformarlo. Eso excluye cualquier característica de TypeScript que en compilación clásica produce JavaScript nuevo:
- Enums (
enum Color { Rojo, Verde }): un enum tradicional no es solo un tipo, es un objeto que existe en tiempo de ejecución. Borrarlo sin más rompería cualquier código que lo use como valor. - Namespaces con valor en runtime: un
namespaceque solo declara tipos se puede borrar sin problema; uno que exporta valores reales (namespace A { export let x = 1; }) necesita generar un objeto contenedor, así que falla con un error explícito (ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX). - Propiedades de parámetro en constructores (
constructor(private nombre: string)), porque esa sintaxis abreviada en realidad genera una asignaciónthis.nombre = nombreque no existe literalmente en el código fuente. - Decoradores, salvo en su forma ya estandarizada por TC39, que sí es sintaxis JavaScript legítima y no necesita transformación TypeScript-específica.
// Esto falla en Node con solo type stripping:
namespace Config {
export let entorno = "produccion"; // valor en runtime, no solo tipo
}
// Esto funciona sin problema: es sintaxis puramente de tipos
namespace Config {
export type Entorno = "produccion" | "desarrollo";
}
tsconfig.json prácticamente no interviene
Node no lee tu tsconfig.json para decidir cómo ejecutar el archivo. Cosas como los alias de rutas (paths) o el downleveling de sintaxis moderna a una versión antigua de ECMAScript simplemente no ocurren: si tu código usa un alias @/utils, Node no sabe resolverlo a menos que uses la resolución de módulos nativa de Node (subpath imports con # en package.json, por ejemplo) en lugar de los alias de TypeScript.
Ningún paquete de node_modules puede publicarse en .ts
El type stripping solo se aplica al código que tú ejecutas directamente. Las dependencias de terceros deben seguir distribuyéndose como .js compilado: Node no va a borrar tipos dentro de node_modules por ti, así que esto no cambia en nada cómo consumes librerías externas.
La configuración recomendada para escribir código pensado para esto
TypeScript incorporó, coincidiendo con la consolidación de esta funcionalidad en Node, una combinación de flags pensada exactamente para este caso: escribir TypeScript que sea “seguro” de ejecutar con solo borrado de tipos, y que tu editor te avise si te sales de esas reglas.
{
"compilerOptions": {
"target": "esnext",
"module": "nodenext",
"verbatimModuleSyntax": true,
"erasableSyntaxOnly": true,
"rewriteRelativeImportExtensions": true,
"noEmit": true
}
}
erasableSyntaxOnlyhace quetscmarque como error cualquier enum, namespace con valor o propiedad de parámetro, exactamente lo que Node rechazaría en runtime. Así el error aparece en tu editor, no en producción.verbatimModuleSyntaxobliga a marcar explícitamente los imports de tipo, evitando el fallo silencioso mencionado antes.rewriteRelativeImportExtensionste deja escribirimport './modulo.ts'con la extensión real del archivo fuente, que es justo lo que Node exige para resolver el módulo sin ambigüedad.noEmittiene sentido si nunca vas a compilar este código a.js:tscse convierte en tu linter de tipos, no en tu compilador.
Cuándo usarlo, y cuándo seguir con un bundler
La otra cara: si tu proyecto usa enums de verdad, decoradores de frameworks como los que dependen de metadata de reflexión, necesita generar un bundle único para desplegar, apunta a un target antiguo de JavaScript, o simplemente ya tiene un pipeline de build maduro que funciona bien, migrar solo por moda no aporta nada. El type stripping no es un sustituto de un bundler como esbuild o del propio tsc en modo compilación: es una alternativa para el caso concreto de “quiero ejecutar este archivo ya, sin ceremonia”.
Vale la pena verlo también en el contexto más amplio de a dónde va TypeScript como proyecto: la consolidación de esta funcionalidad en Node coincide con la migración de todo el compilador a un binario nativo en TypeScript 7, que cubrimos en detalle en TypeScript 6 y el camino a TypeScript 7. Y si estás decidiendo en qué runtime apoyar un backend nuevo, la comparación entre las tres opciones principales está en Bun vs Node.js vs Deno en 2026: merece la pena recordar que tanto Bun como Deno ejecutan TypeScript de forma nativa desde su primer día, con un enfoque distinto al de Node.
La versión corta
El type stripping no convierte a Node.js en un compilador de TypeScript. Es un truco de sustitución de texto extremadamente rápido y deliberadamente limitado a la sintaxis que se puede borrar sin generar nada nuevo. Escribir código pensado para esto significa evitar enums y namespaces con valor, marcar los imports de tipo explícitamente y seguir confiando en tsc --noEmit (o tu editor) para la comprobación de tipos real. A cambio, te libras de una dependencia y de un paso de build para una buena parte de los scripts y servicios que escribes cada semana.
Artículos relacionados
Bun vs Node.js vs Deno en 2026: qué runtime elegir y por qué
Tres runtimes de JavaScript, tres filosofías distintas. Esto es lo que dicen los datos (varios, no uno solo) y cuándo cada uno gana de verdad.
TypeScript 6 y el camino a TypeScript 7: la guía de la migración al compilador nativo
TypeScript 6 fue la última versión escrita en TypeScript; TypeScript 7 reescribe el compilador en Go y multiplica la velocidad por diez. Esto es lo que cambia y cómo prepararte.
Supply chain attacks: cómo protegerte de paquetes npm maliciosos
En 46 minutos, una dependencia comprometida infectó decenas de miles de entornos. Repasamos los vectores reales de los supply chain attacks en 2026, el caso LiteLLM como ejemplo, y las prácticas concretas que de verdad reducen el riesgo.