Back

TypeScript nativo en Node.js: La guía completa para ejecutar archivos .ts sin compilador

Durante más de una década, ejecutar TypeScript en Node.js significaba una cosa: compilar primero, ejecutar después. Ya sea que usaras tsc, ts-node, tsx, esbuild o swc, siempre había un paso intermedio entre tus archivos .ts y la ejecución real. Esa era está terminando.

A partir de Node.js 22 y ahora estable en Node.js 25, puedes ejecutar archivos TypeScript directamente:

node app.ts

Sin paso de build. Sin tsconfig.json requerido. Sin dependencias externas. Solo Node.js y tu código TypeScript.

No es una función experimental ni un nicho. Es un cambio fundamental en el ecosistema Node.js que afecta a cada desarrollador TypeScript. En esta guía cubrimos exactamente cómo funciona, qué puede y no puede hacer, y cómo adoptarlo en proyectos reales.

Cómo funciona: Type Stripping, no Type Checking

La idea clave detrás del enfoque de Node.js es radical en su simplicidad: eliminar los tipos, ejecutar el JavaScript restante.

Node.js no compila TypeScript. No verifica tipos en tu código. No transforma tu sintaxis. Literalmente elimina las anotaciones de tipo y ejecuta lo que queda como JavaScript normal.

Considera este archivo TypeScript:

// app.ts interface User { name: string; age: number; } function greet(user: User): string { return `Hello, ${user.name}! You are ${user.age} years old.`; } const user: User = { name: 'Alice', age: 30 }; console.log(greet(user));

Node.js transforma esto en aproximadamente:

function greet(user) { return `Hello, ${user.name}! You are ${user.age} years old.`; } const user = { name: 'Alice', age: 30 }; console.log(greet(user));

¿La declaración interface User? Desaparece. ¿La anotación de tipo : User? Eliminada. ¿El tipo de retorno : string? Removido. Lo que queda es JavaScript perfectamente válido que V8 puede ejecutar inmediatamente.

Esto se llama "syntaxis borrable" (erasable syntax) — sintaxis TypeScript que puede eliminarse sin cambiar el comportamiento en tiempo de ejecución del código. Y cubre la gran mayoría de las funcionalidades TypeScript que los desarrolladores usan a diario.

La implementación: Amaro y SWC

Internamente, Node.js usa una librería llamada Amaro para realizar el type stripping. Amaro es un wrapper ligero alrededor de @swc/wasm-typescript — la build WebAssembly del parser TypeScript de SWC.

El pipeline se ve así:

tu-archivo.ts → Amaro (SWC WASM) → JS sin tipos → ejecución V8

Esta arquitectura tiene varias implicaciones importantes:

  1. Es rápido. SWC está escrito en Rust y compilado a WebAssembly. La operación de type stripping es órdenes de magnitud más rápida que una compilación completa con tsc porque solo parsea y elimina — no resuelve tipos, no verifica restricciones ni emite archivos de declaración.

  2. Viene integrado. Amaro se distribuye con Node.js. No necesitas npm install. No contamina node_modules. Es parte del runtime.

  3. Está limitado por diseño. Como usa SWC para stripping (no compilación completa), solo la sintaxis TypeScript borrable está soportada en el modo predeterminado.

La línea temporal: de experimental a estable

El camino hacia TypeScript nativo ha sido sistemático:

VersiónHito
Node.js 22.6.0 (Julio 2024)Flag --experimental-strip-types introducido
Node.js 22.7.0 (Agosto 2024)--experimental-transform-types añadido para enums
Node.js 23.x (Finales 2024)Refinamientos, testing extensivo
Node.js 22.18.0 / 23.6.0 (2025)Type stripping habilitado por defecto (sin flag)
Node.js 25.2.0 (2026)Release estable, warnings experimentales eliminados

A principios de 2026, si estás ejecutando Node.js 22.18+ o cualquier versión reciente, el type stripping funciona out of the box. Sin flags, sin configuración.

Qué funciona: Sintaxis TypeScript borrable

Las siguientes funcionalidades de TypeScript funcionan perfectamente con el type stripping de Node.js porque pueden eliminarse sin afectar el comportamiento en runtime:

Anotaciones de tipo

// Todo esto simplemente se elimina const name: string = 'hello'; function add(a: number, b: number): number { return a + b; } const items: Array<string> = ['a', 'b', 'c'];

Interfaces y alias de tipo

interface Config { host: string; port: number; debug?: boolean; } type DatabaseURL = `postgres://${string}`; // Son completamente borrados — no existen en runtime

Genéricos

function identity<T>(value: T): T { return value; } class Container<T> { constructor(private value: T) {} get(): T { return this.value; } }

Type assertions y casts con as

const input = document.getElementById('name') as HTMLInputElement; const data = JSON.parse(body) as ApiResponse;

Operador satisfies

const config = { host: 'localhost', port: 3000, } satisfies Config;

Tipos utilitarios

type ReadonlyUser = Readonly<User>; type PartialConfig = Partial<Config>; type UserKeys = keyof User; // Todo borrado en el momento del type stripping

Qué no funciona: Sintaxis no borrable

Aquí es donde las cosas se ponen más complicadas. Algunas funcionalidades de TypeScript producen código en runtime — no pueden simplemente eliminarse porque hacerlo cambiaría el comportamiento. Estas se llaman funcionalidades "no borrables".

Enums (clásicos)

// ❌ Error en runtime solo con type stripping enum Direction { Up = 'UP', Down = 'DOWN', Left = 'LEFT', Right = 'RIGHT', }

Los enums generan objetos JavaScript en runtime. Eliminar la palabra clave enum dejaría sintaxis inválida. Para usar enums necesitas el flag --experimental-transform-types:

node --experimental-transform-types app.ts

Mejor alternativa: Usa objetos as const en lugar de enums:

// ✅ Funciona con type stripping normal const Direction = { Up: 'UP', Down: 'DOWN', Left: 'LEFT', Right: 'RIGHT', } as const; type Direction = typeof Direction[keyof typeof Direction];

Propiedades de parámetros

// ❌ Requiere --experimental-transform-types class User { constructor(public name: string, private age: number) {} }

Las propiedades de parámetros (public, private, protected, readonly en parámetros del constructor) generan código de asignación. El atajo public name: string se convierte en this.name = name; en el cuerpo del constructor. Esta transformación va más allá del stripping.

Alternativa:

// ✅ Funciona con type stripping normal class User { name: string; // Anotación de tipo — se elimina private _age: number; constructor(name: string, age: number) { this.name = name; this._age = age; } }

Decoradores legacy y emitDecoratorMetadata

// ❌ No soportado — generan metadata en runtime @Controller('/users') class UserController { @Get('/:id') getUser(@Param('id') id: string) { ... } }

Los decoradores legacy (experimentales) y emitDecoratorMetadata emiten metadata de reflexión en runtime. Esto es común en frameworks como NestJS, TypeORM y Angular.

Nota: Los decoradores TC39 Stage 3 (el estándar moderno) son una funcionalidad de JavaScript y se manejan por separado en el motor JavaScript de Node.js.

Namespaces con fusión en runtime

// ❌ No soportado — los namespaces generan IIFEs namespace Validation { export function isEmail(str: string): boolean { return str.includes('@'); } }

Alternativa: Usa módulos ES en su lugar:

// validation.ts export function isEmail(str: string): boolean { return str.includes('@'); }

Restricciones importantes y trampas

No hay verificación de tipos

Lo más importante a entender: Node.js no verifica los tipos de tu código. Si tienes un error de tipos:

const name: number = "hello"; // ¡Error de tipos!

Node.js eliminará felizmente la anotación : number y ejecutará el código. No tiene idea (ni interés) de si los tipos son correctos. Eso sigue siendo trabajo de tsc.

Tu flujo de trabajo se convierte en:

# Desarrollo: simplemente ejecútalo node app.ts # CI/CD: verificación de tipos por separado npx tsc --noEmit

Esto es en realidad una mejora significativa de velocidad. Durante el desarrollo, te saltas la verificación de tipos por completo y obtienes ejecución instantánea. La verificación de tipos ocurre en tu editor (via el language server de TypeScript) y en CI.

Especificadores de import: .ts vs .js

Este es uno de los problemas más complicados de la migración. Node.js requiere extensiones de archivo explícitas en imports cuando usas ES modules:

// ❌ Ambiguo — Node.js no sabe a qué archivo te refieres import { greet } from './utils'; // ✅ Extensión .ts explícita import { greet } from './utils.ts';

La solución: TypeScript 5.7+ introdujo la opción rewriteRelativeImportExtensions del compilador:

{ "compilerOptions": { "rewriteRelativeImportExtensions": true } }

Esto indica a tsc que acepte especificadores de import con .ts y los reescriba a .js en la salida.

Los path aliases de tsconfig.json no funcionan

// ❌ Node.js no lee tsconfig.json import { db } from '@/database';

Node.js no lee ni procesa tsconfig.json. Los path aliases son una funcionalidad de compile-time. Las alternativas incluyen:

  1. Subpath imports de Node.js en package.json:
{ "imports": { "#src/*": "./src/*" } }
import { db } from '#src/database.ts';
  1. Imports relativos (el enfoque más simple para la mayoría de los casos)

Migración real: de ts-node a nativo

Antes: setup con ts-node

// package.json { "scripts": { "dev": "ts-node --esm src/index.ts", "start": "node dist/index.js", "build": "tsc" }, "devDependencies": { "typescript": "^5.5.0", "ts-node": "^10.9.0", "@types/node": "^22.0.0" } }

Después: Node.js TypeScript nativo

// package.json { "scripts": { "dev": "node --watch src/index.ts", "start": "node src/index.ts", "typecheck": "tsc --noEmit", "build": "tsc" }, "devDependencies": { "typescript": "^5.7.0", "@types/node": "^22.0.0" } }

Observa qué cambió:

  1. ts-node desapareció. Eliminado por completo de devDependencies.
  2. El script dev es simplemente node. Combinado con --watch para modo vigilancia integrado.
  3. start ejecuta .ts directamente. No más compilación a dist/ para desarrollo.
  4. typecheck está separado. La verificación de tipos se convierte en un paso explícito y opcional.
  5. build sigue usando tsc. Para builds de producción donde necesitas archivos de declaración o dirigirte a runtimes más antiguos.

Pasos de migración

  1. Actualiza Node.js a 22.18+ (o 25+ para estable sin warnings).

  2. Elimina ts-node y tsx:

npm uninstall ts-node tsx
  1. Actualiza las extensiones de import a .ts:
// Antes import { db } from './database.js'; // Después import { db } from './database.ts';
  1. Reemplaza enums con objetos as const (o usa --experimental-transform-types).

  2. Habilita erasableSyntaxOnly en tsconfig (TypeScript 5.8+):

{ "compilerOptions": { "erasableSyntaxOnly": true } }

Este flag indica a tsc que rechace cualquier sintaxis no borrable (enums, propiedades de parámetros, namespaces) en tiempo de compilación, asegurando que tu código siempre sea compatible con el type stripping de Node.js. Si accidentalmente usas una funcionalidad que Node.js no puede manejar, TypeScript lo detectará antes del runtime.

  1. Actualiza los scripts en package.json.

  2. Añade typecheck al CI:

# GitHub Actions - name: Type Check run: npx tsc --noEmit

¿Deberías adoptarlo hoy?

Aquí tienes una matriz de decisión:

EscenarioRecomendación
Proyecto nuevo, Node.js moderno✅ Usa TypeScript nativo
Herramientas CLI y scripts✅ Caso de uso perfecto — cero paso de build
Desarrollo/prototipado✅ Iteración más rápida posible
Servidor API (Express, Fastify)✅ Funciona genial, añade tsc en CI
NestJS / TypeORM (decoradores)⚠️ Espera — decoradores legacy no soportados
Librería con archivos de declaración⚠️ Todavía necesitas tsc para generar .d.ts
Builds de producción para Node.js antiguo❌ Mantén tsc para compilación

El flujo de trabajo híbrido recomendado

Para la mayoría de proyectos en 2026, el flujo de trabajo óptimo es:

Desarrollo:  node --watch app.ts     (instantáneo, sin build)
Editor:      TypeScript LSP          (verificación de tipos en tiempo real)
CI:          tsc --noEmit            (verificación de tipos estricta)
Producción:  node app.ts             (ejecución directa)

Impacto en el ecosistema

El soporte nativo de TypeScript en Node.js tiene efectos dominó en todo el ecosistema:

Herramientas que se vuelven menos necesarias:

  • ts-node — el reemplazo más directo; en gran parte innecesario para desarrollo
  • tsx — la alternativa más rápida a ts-node; misma situación
  • esbuild / swc como transpiladores de desarrollo — Node.js ahora maneja esto

Herramientas que siguen siendo esenciales:

  • tsc — para verificación de tipos, generación de archivos de declaración y dirigirse a entornos más antiguos
  • Bundlers (Vite, webpack, Rollup) — para código del navegador, tree-shaking y optimización
  • @swc/core / esbuild — para pipelines de build en producción donde necesitas transformación completa

Frameworks adaptándose:

  • Deno y Bun ya tenían soporte nativo para TypeScript. Node.js uniéndose al club nivela el terreno de juego.
  • NestJS está explorando los decoradores TC39 (que no necesitan transformación de tipos) como ruta de migración.
  • Express y Fastify funcionan perfectamente con type stripping nativo — no necesitan cambios.

Mirando hacia adelante

El soporte nativo de TypeScript en Node.js refleja una tendencia más amplia: la línea entre TypeScript y JavaScript se está borrando. TypeScript ya no es un lenguaje de "compilar a JS" en el sentido clásico. Se está convirtiendo en un dialecto de JavaScript que los runtimes entienden de forma nativa.

El equipo de TypeScript está acelerando esta convergencia. TypeScript 5.8 añadió el flag --erasableSyntaxOnly para forzar compatibilidad con Node.js en tiempo de compilación. TypeScript 6.0 Beta salió en febrero de 2026 como una release de transición. Y lo más ambicioso es TypeScript 7.0 (Project Corsa): una reescritura completa del compilador y servicio de lenguaje en Go, apuntando a una mejora de rendimiento de 10x. Con fecha para mediados de 2026, Project Corsa haría tsc --noEmit tan rápido que el argumento de "saltarse la verificación de tipos" podría volverse irrelevante.

Mientras tanto, la propuesta TC39 Type Annotations (Stage 1) busca hacer las anotaciones de tipo parte de la especificación de JavaScript. Si avanza, los navegadores y runtimes ignorarían las anotaciones de tipo de forma nativa, exactamente lo que Node.js ya hace hoy con Amaro.

Nos movemos hacia un mundo donde la pregunta no es "¿Cómo compilo TypeScript?" sino "¿Por qué necesitaría compilar?"

Para la mayoría de las aplicaciones, la respuesta es cada vez más: no lo necesitas.

Conclusión

El soporte nativo de TypeScript en Node.js no es solo una función de conveniencia, es un cambio de paradigma. El ciclo de desarrollo se reduce de editar → compilar → ejecutar a editar → ejecutar. El árbol de dependencias se aligera. La carga mental de la configuración del build desaparece.

Si estás empezando un nuevo proyecto Node.js en 2026, no hay razón para montar un pipeline de compilación TypeScript para desarrollo. Escribe archivos .ts y ejecútalos con node. Guarda tsc para la verificación de tipos en CI y para los casos donde necesites archivos de declaración o salida compilada.

El futuro de TypeScript en Node.js no va de mejores compiladores. Va de no necesitar uno en absoluto.

Node.jsTypeScriptjavascriptnodejsbackenddeveloper-toolsbuild-tools

Explora herramientas relacionadas

Prueba estas herramientas gratuitas de Pockit