Volver al blog

Node.js 25: TypeScript Nativo Sin Transpilación Cambia Todo en el Backend JavaScript

Hola HaWkers, uno de los cambios más aguardados por la comunidad JavaScript finalmente llegó de forma madura en Node.js 25. Ahora puedes ejecutar archivos TypeScript directamente con node index.ts sin precisar ts-node, Babel o cualquier etapa de build.

¿Ya pensaste en eliminar completamente el paso de transpilación de tu workflow de desarrollo? Esa realidad llegó.

Qué Cambió en Node.js 25

Node.js 25, lanzado como versión Current en noviembre de 2025, consolida el soporte nativo a TypeScript que comenzó experimental en versiones anteriores. La feature ahora está estable y lista para uso en producción.

Cómo Funciona

Node.js usa una técnica llamada type stripping - él remueve los tipos del TypeScript en tiempo de ejecución, sin hacer ninguna transformación de código. Esto significa que tu código TypeScript ejecuta prácticamente en la misma velocidad que JavaScript puro.

# Antes - workflow tradicional con TypeScript
npx tsc src/index.ts --outDir dist
node dist/index.js

# O con ts-node
npx ts-node src/index.ts

# Ahora - Node.js 25+
node src/index.ts

Configuración Zero

La belleza de esta feature es que no requiere configuración especial:

# Instalación de Node.js 25
nvm install 25
nvm use 25

# Crea un archivo TypeScript
echo 'const message: string = "Hello, TypeScript!";
console.log(message);' > hello.ts

# Ejecuta directamente
node hello.ts
# Output: Hello, TypeScript!

Ejemplos Prácticos

API Express con TypeScript

Ve cómo queda simple crear una API Express con tipado completo:

// server.ts - ejecuta con: node server.ts
import express, { Request, Response, NextFunction } from 'express';

interface User {
  id: number;
  name: string;
  email: string;
  createdAt: Date;
}

interface ApiResponse<T> {
  success: boolean;
  data: T;
  message?: string;
}

const app = express();
app.use(express.json());

// Base de datos simulada con tipado
const users: Map<number, User> = new Map();
let nextId = 1;

// Middleware de logging tipado
const logRequest = (req: Request, res: Response, next: NextFunction): void => {
  console.log(`[${new Date().toISOString()}] ${req.method} ${req.path}`);
  next();
};

app.use(logRequest);

// GET /users - Lista todos los usuarios
app.get('/users', (req: Request, res: Response<ApiResponse<User[]>>) => {
  const allUsers = Array.from(users.values());

  res.json({
    success: true,
    data: allUsers
  });
});

// POST /users - Crea un nuevo usuario
app.post('/users', (req: Request, res: Response<ApiResponse<User>>) => {
  const { name, email } = req.body as { name: string; email: string };

  const newUser: User = {
    id: nextId++,
    name,
    email,
    createdAt: new Date()
  };

  users.set(newUser.id, newUser);

  res.status(201).json({
    success: true,
    data: newUser,
    message: 'User created successfully'
  });
});

// GET /users/:id - Busca usuario por ID
app.get('/users/:id', (req: Request, res: Response<ApiResponse<User | null>>) => {
  const userId = parseInt(req.params.id);
  const user = users.get(userId);

  if (!user) {
    res.status(404).json({
      success: false,
      data: null,
      message: 'User not found'
    });
    return;
  }

  res.json({
    success: true,
    data: user
  });
});

const PORT = process.env.PORT || 3000;

app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
  console.log(`Try: curl http://localhost:${PORT}/users`);
});

Scripts de Automatización Tipados

Para scripts de automatización y herramientas CLI, el TypeScript nativo es perfecto:

// deploy-script.ts - ejecuta con: node deploy-script.ts
import { execSync } from 'child_process';
import { readFileSync, existsSync } from 'fs';
import { join } from 'path';

interface DeployConfig {
  environment: 'staging' | 'production';
  branch: string;
  buildCommand: string;
  deployTarget: string;
}

interface DeployResult {
  success: boolean;
  duration: number;
  commitHash: string;
  timestamp: Date;
}

function loadConfig(env: string): DeployConfig {
  const configPath = join(process.cwd(), `deploy.${env}.json`);

  if (!existsSync(configPath)) {
    throw new Error(`Config file not found: ${configPath}`);
  }

  const content = readFileSync(configPath, 'utf-8');
  return JSON.parse(content) as DeployConfig;
}

function executeCommand(command: string): string {
  console.log(`Executing: ${command}`);
  return execSync(command, { encoding: 'utf-8' });
}

function getCurrentCommit(): string {
  return executeCommand('git rev-parse HEAD').trim();
}

async function deploy(environment: string): Promise<DeployResult> {
  const startTime = Date.now();

  console.log(`\n🚀 Starting deployment to ${environment}\n`);

  const config = loadConfig(environment);
  const commitHash = getCurrentCommit();

  console.log(`📋 Configuration loaded:`);
  console.log(`   Environment: ${config.environment}`);
  console.log(`   Branch: ${config.branch}`);
  console.log(`   Commit: ${commitHash.substring(0, 8)}`);

  // Verificar branch
  const currentBranch = executeCommand('git branch --show-current').trim();
  if (currentBranch !== config.branch) {
    throw new Error(
      `Wrong branch! Expected ${config.branch}, got ${currentBranch}`
    );
  }

  // Build
  console.log(`\n🔨 Building...`);
  executeCommand(config.buildCommand);

  // Deploy
  console.log(`\n📦 Deploying to ${config.deployTarget}...`);
  executeCommand(`rsync -avz ./dist/ ${config.deployTarget}`);

  const duration = Date.now() - startTime;

  const result: DeployResult = {
    success: true,
    duration,
    commitHash,
    timestamp: new Date()
  };

  console.log(`\n✅ Deployment completed in ${duration}ms`);

  return result;
}

// Ejecución
const env = process.argv[2] || 'staging';

deploy(env)
  .then(result => {
    console.log('\nDeploy Result:', result);
  })
  .catch(error => {
    console.error('\n❌ Deployment failed:', error.message);
    process.exit(1);
  });

Worker Threads con TypeScript

El soporte a Worker Threads también funciona perfectamente:

// main.ts
import { Worker, isMainThread, parentPort, workerData } from 'worker_threads';
import { cpus } from 'os';

interface WorkerTask {
  id: number;
  data: number[];
  operation: 'sum' | 'average' | 'max' | 'min';
}

interface WorkerResult {
  id: number;
  result: number;
  processedBy: number;
}

if (isMainThread) {
  // Código principal
  const numCPUs = cpus().length;
  console.log(`Main thread running with ${numCPUs} CPU cores available`);

  const tasks: WorkerTask[] = [
    { id: 1, data: [1, 2, 3, 4, 5], operation: 'sum' },
    { id: 2, data: [10, 20, 30, 40, 50], operation: 'average' },
    { id: 3, data: [5, 15, 3, 22, 8], operation: 'max' },
    { id: 4, data: [100, 50, 75, 25, 200], operation: 'min' }
  ];

  const processTask = (task: WorkerTask): Promise<WorkerResult> => {
    return new Promise((resolve, reject) => {
      // Worker ejecutando el mismo archivo .ts
      const worker = new Worker(new URL(import.meta.url), {
        workerData: task
      });

      worker.on('message', resolve);
      worker.on('error', reject);
    });
  };

  // Procesa todas las tasks en paralelo
  Promise.all(tasks.map(processTask))
    .then(results => {
      console.log('\nResults:');
      results.forEach(r => {
        console.log(`  Task ${r.id}: ${r.result} (Worker ${r.processedBy})`);
      });
    });

} else {
  // Código del Worker
  const task = workerData as WorkerTask;

  const operations = {
    sum: (arr: number[]) => arr.reduce((a, b) => a + b, 0),
    average: (arr: number[]) => arr.reduce((a, b) => a + b, 0) / arr.length,
    max: (arr: number[]) => Math.max(...arr),
    min: (arr: number[]) => Math.min(...arr)
  };

  const result: WorkerResult = {
    id: task.id,
    result: operations[task.operation](task.data),
    processedBy: process.pid
  };

  parentPort?.postMessage(result);
}

Otras Novedades de Node.js 25

Además del TypeScript nativo, Node.js 25 trae otras mejoras significativas.

V8 Engine 13.6

La nueva versión del V8 trae optimizaciones de performance:

// Mejoras en Array methods
const numbers: number[] = Array.from({ length: 1_000_000 }, (_, i) => i);

// Array.prototype.at() optimizado
const last = numbers.at(-1);  // Más rápido que numbers[numbers.length - 1]

// Iteradores optimizados
const doubled = numbers.values().map(n => n * 2).toArray();

// Object.groupBy() con mejor performance
interface Product {
  category: string;
  name: string;
  price: number;
}

const products: Product[] = [
  { category: 'electronics', name: 'Phone', price: 999 },
  { category: 'electronics', name: 'Laptop', price: 1499 },
  { category: 'clothing', name: 'Shirt', price: 49 }
];

const grouped = Object.groupBy(products, p => p.category);
// { electronics: [...], clothing: [...] }

Permission Model Mejorado

El modelo de permisos está más robusto:

# Ejecutar con permisos restrictos
node --permission --allow-fs-read=/app/config --allow-fs-write=/app/logs server.ts

# Permitir apenas acceso de red específico
node --permission --allow-net=api.example.com:443 client.ts

npm 11 Integrado

npm 11 viene incluido con mejoras:

# Workspaces más rápidos
npm install --workspaces

# Mejor resolución de dependencias
npm install --prefer-dedupe

# Audit mejorado
npm audit --json

Limitaciones y Consideraciones

Aunque revolucionario, el TypeScript nativo en Node.js tiene algunas limitaciones importantes.

Lo Que NO Funciona

1. Enums no son soportados

// ❌ No funciona
enum Status {
  Active = 'active',
  Inactive = 'inactive'
}

// ✅ Usa const objects o union types
const Status = {
  Active: 'active',
  Inactive: 'inactive'
} as const;

type StatusType = typeof Status[keyof typeof Status];

2. Decorators experimentales

// ❌ Decorators no funcionan nativamente
@Injectable()
class UserService {}

// ✅ Usa patrones alternativos o continúa con transpilación

3. Namespaces TypeScript

// ❌ Namespaces no soportados
namespace MyApp {
  export interface User {}
}

// ✅ Usa ES Modules
// user.ts
export interface User {}

Cuándo Continuar Usando Build Step

Existen escenarios donde la transpilación todavía es necesaria:

Para Producción con Optimización:

  • Minificación de código
  • Tree-shaking
  • Bundle único para deploy

Para Features Avanzadas:

  • Decorators (NestJS, TypeORM)
  • Paths/aliases complejos
  • JSX/TSX (React)

Comparación de Workflows

Aspecto Build Tradicional Node.js 25 Nativo
Setup inicial Complejo Zero
Tiempo de dev Más lento Instantáneo
Debug Source maps Directo
Hot reload Configuración Simple
Performance runtime Igual Igual
Features TS Todas Mayoría

Migración Gradual

Si tienes un proyecto existente, puedes migrar gradualmente:

// package.json
{
  "name": "my-project",
  "type": "module",
  "engines": {
    "node": ">=25.0.0"
  },
  "scripts": {
    "dev": "node --watch src/index.ts",
    "start": "node src/index.ts",
    "build": "tsc",
    "start:prod": "node dist/index.js"
  }
}
// tsconfig.json - configuración mínima
{
  "compilerOptions": {
    "target": "ES2024",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "strict": true,
    "skipLibCheck": true,
    "declaration": true,
    "outDir": "./dist"
  },
  "include": ["src/**/*"]
}

Conclusión

Node.js 25 representa un hito en la historia de JavaScript en el servidor. La capacidad de ejecutar TypeScript nativamente elimina una de las mayores fricciones del desarrollo moderno y aproxima la experiencia de lenguajes como Deno y Bun.

Para desarrolladores, esto significa menos configuración, feedback más rápido durante el desarrollo y una barrera de entrada menor para nuevos proyectos. Es el tipo de cambio que simplifica nuestra vida sin sacrificar las ventajas del tipado estático.

Si todavía no experimentaste, recomiendo crear un proyecto de prueba y sentir la diferencia. Para profundizar más en TypeScript, confiere nuestro artículo sobre State of JavaScript 2025 que trae insights sobre la adopción de TypeScript en el ecosistema.

¡Vamos a por ello! 🦅

Comentarios (0)

Este artículo aún no tiene comentarios 😢. ¡Sé el primero! 🚀🦅

Añadir comentarios