Volver al blog

TypeScript Nativo en Node.js: La Revolución del --experimental-strip-types

Hola HaWkers, 2025 marcó el inicio de una nueva era para desarrolladores TypeScript: ejecutar código TypeScript directamente en Node.js, sin transpilación previa.

¿Ya te preguntaste por qué necesitamos compilar TypeScript para JavaScript antes de ejecutar? ¿Y si Node.js pudiera simplemente... ignorar los tipos y ejecutar directo?

El Gran Cambio en Node.js 22+

Con Node.js 22, una funcionalidad experimental revolucionaria fue introducida: --experimental-strip-types. Esta flag permite a Node.js ejecutar archivos TypeScript nativamente, removiendo anotaciones de tipo en tiempo de ejecución.

El Problema Tradicional

Históricamente, trabajar con TypeScript en Node.js siempre involucró múltiples etapas:

# Flujo tradicional (todavía común en 2024)
# 1. Escribir TypeScript
# 2. Compilar con tsc
npx tsc

# 3. Ejecutar el JavaScript generado
node dist/index.js

# O usar herramientas como ts-node
npx ts-node src/index.ts

Herramientas como ts-node, tsx, y ts-node-dev surgieron para simplificar, pero todavía dependían de transpilación en memoria.

Cómo Funciona el --experimental-strip-types

Node.js 22+ introdujo un abordaje radicalmente diferente: en vez de transpilar TypeScript para JavaScript, simplemente remueve las anotaciones de tipo y ejecuta el código.

Ejemplo Práctico

// user.ts - Código TypeScript puro
interface User {
  id: number;
  name: string;
  email: string;
  createdAt: Date;
}

class UserService {
  private users: Map<number, User> = new Map();

  createUser(name: string, email: string): User {
    const id = this.users.size + 1;
    const user: User = {
      id,
      name,
      email,
      createdAt: new Date()
    };

    this.users.set(id, user);
    return user;
  }

  getUser(id: number): User | undefined {
    return this.users.get(id);
  }

  getAllUsers(): User[] {
    return Array.from(this.users.values());
  }
}

// Uso
const service = new UserService();
const user = service.createUser('Jeff', 'jeff@example.com');
console.log(user);

Antes (Node.js 20 y anterior):

# Necesitaba compilar primero
npx tsc user.ts
node user.js

# O usar ts-node
npx ts-node user.ts

Ahora (Node.js 22+):

# ¡Ejecución directa!
node --experimental-strip-types user.ts

Qué Está Ocurriendo Por Debajo

Node.js no está compilando TypeScript. Está haciendo algo más simple y rápido:

// TypeScript original
function greet(name: string): string {
  return `Hello, ${name}!`;
}

const user: User = { name: 'Jeff', age: 30 };

// Lo que Node.js realmente ejecuta (tipos removidos)
function greet(name) {
  return `Hello, ${name}!`;
}

const user = { name: 'Jeff', age: 30 };

Configuración Moderna para TypeScript + Node.js en 2025

Vamos a crear un proyecto moderno aprovechando estas nuevas capacidades:

1. Inicialización del Proyecto

# Crear directorio
mkdir modern-node-ts
cd modern-node-ts

# Inicializar package.json
npm init -y

# Instalar TypeScript (solo para type checking)
npm install -D typescript @types/node

# Crear tsconfig.json
npx tsc --init

2. Configuración del tsconfig.json

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "lib": ["ES2022"],
    "moduleResolution": "bundler",
    "esModuleInterop": true,
    "strict": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,

    // Para type checking solamente, no genera output
    "noEmit": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}

3. Package.json con Scripts Modernos

{
  "name": "modern-node-ts",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "dev": "node --experimental-strip-types --watch src/index.ts",
    "start": "node --experimental-strip-types src/index.ts",
    "type-check": "tsc --noEmit",
    "check": "tsc --noEmit && echo '✓ Type check passed'"
  },
  "devDependencies": {
    "@types/node": "^22.0.0",
    "typescript": "^5.6.0"
  }
}

4. Ejemplo de Aplicación Completa

// src/config.ts
export interface AppConfig {
  port: number;
  host: string;
  env: 'development' | 'production' | 'test';
}

export const config: AppConfig = {
  port: parseInt(process.env.PORT || '3000'),
  host: process.env.HOST || 'localhost',
  env: (process.env.NODE_ENV as AppConfig['env']) || 'development'
};

// src/database.ts
export interface DatabaseRecord {
  id: string;
  createdAt: Date;
  updatedAt: Date;
}

export class InMemoryDatabase<T extends DatabaseRecord> {
  private records: Map<string, T> = new Map();

  async create(data: Omit<T, 'id' | 'createdAt' | 'updatedAt'>): Promise<T> {
    const record = {
      ...data,
      id: crypto.randomUUID(),
      createdAt: new Date(),
      updatedAt: new Date()
    } as T;

    this.records.set(record.id, record);
    return record;
  }

  async findById(id: string): Promise<T | null> {
    return this.records.get(id) || null;
  }

  async findAll(): Promise<T[]> {
    return Array.from(this.records.values());
  }

  async update(id: string, data: Partial<T>): Promise<T | null> {
    const record = this.records.get(id);
    if (!record) return null;

    const updated = {
      ...record,
      ...data,
      updatedAt: new Date()
    };

    this.records.set(id, updated);
    return updated;
  }

  async delete(id: string): Promise<boolean> {
    return this.records.delete(id);
  }
}

// src/user.ts
import type { DatabaseRecord } from './database.js';
import { InMemoryDatabase } from './database.js';

export interface User extends DatabaseRecord {
  name: string;
  email: string;
  role: 'admin' | 'user' | 'guest';
}

export class UserRepository extends InMemoryDatabase<User> {
  async findByEmail(email: string): Promise<User | null> {
    const users = await this.findAll();
    return users.find(u => u.email === email) || null;
  }

  async isAdmin(userId: string): Promise<boolean> {
    const user = await this.findById(userId);
    return user?.role === 'admin';
  }
}

// src/index.ts
import { createServer } from 'http';
import { config } from './config.js';
import { UserRepository } from './user.js';

const userRepo = new UserRepository();

// Crear algunos usuarios de ejemplo
const admin = await userRepo.create({
  name: 'Admin User',
  email: 'admin@example.com',
  role: 'admin'
});

const regularUser = await userRepo.create({
  name: 'Jeff Bruchado',
  email: 'jeff@example.com',
  role: 'user'
});

console.log('📦 Users created:', { admin, regularUser });

// Servidor HTTP simple
const server = createServer(async (req, res) => {
  const url = new URL(req.url || '/', `http://${req.headers.host}`);

  if (url.pathname === '/users' && req.method === 'GET') {
    const users = await userRepo.findAll();
    res.writeHead(200, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify(users, null, 2));
    return;
  }

  if (url.pathname.startsWith('/users/') && req.method === 'GET') {
    const id = url.pathname.split('/')[2];
    const user = await userRepo.findById(id);

    if (!user) {
      res.writeHead(404, { 'Content-Type': 'application/json' });
      res.end(JSON.stringify({ error: 'User not found' }));
      return;
    }

    res.writeHead(200, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify(user, null, 2));
    return;
  }

  res.writeHead(404, { 'Content-Type': 'application/json' });
  res.end(JSON.stringify({ error: 'Route not found' }));
});

server.listen(config.port, config.host, () => {
  console.log(`🚀 Server running at http://${config.host}:${config.port}`);
  console.log(`📝 Environment: ${config.env}`);
  console.log('\nAvailable routes:');
  console.log('  GET /users');
  console.log('  GET /users/:id');
});

Ahora ejecuta:

# Desarrollo con hot reload
npm run dev

# Type checking
npm run check

# Producción
npm start

Ventajas del Approach Nativo

1. Startup Más Rápido

Sin transpilación significa inicialización más rápida, especialmente importante para:

  • Scripts CLI
  • Funciones serverless
  • Tests unitarios
  • Desarrollo local
# Comparación de tiempo de startup
# ts-node (transpilación JIT)
time ts-node src/index.ts
# ~450ms

# Node.js 22+ con strip-types
time node --experimental-strip-types src/index.ts
# ~120ms 🚀

2. Setup Minimalista

{
  "devDependencies": {
    "typescript": "^5.6.0",  // Solo para type checking
    "@types/node": "^22.0.0"
  }
}

No necesita: ts-node, tsx, @swc/core, esbuild, etc.

3. Watch Mode Nativo

# Watch mode built-in de Node.js
node --experimental-strip-types --watch src/index.ts

Detecta cambios y reinicia automáticamente, sin herramientas externas.

4. Debugging Directo

# Debugging en VS Code funciona nativamente
node --experimental-strip-types --inspect src/index.ts

Limitaciones y Consideraciones

Qué NO Funciona (Todavía)

1. Transformaciones de TypeScript

// ❌ Enums no funcionan (necesitan transformación)
enum Status {
  Active = 'ACTIVE',
  Inactive = 'INACTIVE'
}

// ✅ Usa union types
type Status = 'ACTIVE' | 'INACTIVE';

// ❌ Decorators no funcionan
class UserController {
  @Get('/users')
  getUsers() {}
}

// ✅ Usa patterns alternativos
const routes = {
  getUsers: {
    method: 'GET',
    path: '/users',
    handler: getUsers
  }
};

2. Namespace Syntax

// ❌ Namespaces no son soportados
namespace Utils {
  export function formatDate() {}
}

// ✅ Usa modules ES6
export function formatDate() {}

3. Parámetros Opcionales en Interfaces Implementadas

Funciona, pero ten cuidado con compatibilidad.

Cuándo Todavía Usar Build Tools

Todavía necesitas herramientas como esbuild, swc, o tsc para:

  • Producción con bundling
  • Compatibilidad con Node.js antiguo
  • Uso de features TypeScript avanzadas (enums, decorators)
  • Optimizaciones de bundle size
// package.json para setup híbrido
{
  "scripts": {
    "dev": "node --experimental-strip-types --watch src/index.ts",
    "build": "tsc",
    "start": "node dist/index.js",
    "type-check": "tsc --noEmit"
  }
}

Comparación: Strip Types vs Alternativas

Node.js Strip Types

# Pros:
# - Cero dependencies extras
# - Startup rápido
# - Watch mode nativo
# - Debugging directo

# Cons:
# - Experimental (puede cambiar)
# - Sin transformaciones (enums, decorators)
# - Requiere Node.js 22+

tsx (Sucesor de ts-node)

npm install -D tsx

# Pros:
# - Muy rápido (usa esbuild)
# - Soporta todas features TS
# - Funciona en Node.js antiguo
# - Estable y maduro

# Cons:
# - Dependencia extra
# - Transpilación JIT (overhead pequeño)

Bun

# Pros:
# - TypeScript nativo sin flags
# - Extremadamente rápido
# - Runtime completo

# Cons:
# - Runtime diferente de Node.js
# - Compatibilidad puede variar

Deno

# Pros:
# - TypeScript first-class
# - Secure by default
# - Herramientas built-in

# Cons:
# - Ecosistema diferente
# - Requiere cambios en el código

El Futuro de TypeScript en Node.js

Roadmap Esperado

2025-2026:

  • Strip types sale de experimental
  • Soporte a más syntax TypeScript
  • Performance optimizada

2027+:

  • TypeScript como ciudadano de primera clase
  • Posible soporte a type checking en runtime (opt-in)
  • Integración completa con tooling de Node.js
// Visión del futuro (especulativo)
const futureNodeTS = {
  native_typescript: true,
  strip_types: 'stable',

  optional_features: {
    runtime_type_checking: true, // opt-in
    enum_support: true,
    decorator_support: true
  },

  performance: {
    startup: 'near-javascript speed',
    execution: 'zero overhead',
    memory: 'minimal'
  },

  developer_experience: {
    setup_complexity: 'minimal',
    tooling_integration: 'seamless',
    debugging: 'native'
  }
};

Adopción de la Industria

TypeScript se está convirtiendo en el estándar:

  • 38.5% de los desarrolladores ya usan TypeScript
  • Top 5 lenguajes más populares
  • Mayoría de los nuevos proyectos Node.js comienzan con TS
// Patrón emergente en 2025
// Proyectos comienzan directo en TypeScript

// package.json
{
  "name": "my-new-project",
  "type": "module",
  "scripts": {
    "dev": "node --experimental-strip-types --watch src/index.ts",
    "build": "tsc",
    "start": "node dist/index.js"
  }
}

Migración de Proyecto Existente

Paso a Paso

# 1. Actualizar para Node.js 22+
nvm install 22
nvm use 22

# 2. Actualizar package.json
# Añadir script de dev

# 3. Testar ejecución directa
node --experimental-strip-types src/index.ts

# 4. Verificar features incompatibles
# - Buscar enums
# - Buscar namespaces
# - Buscar decorators

# 5. Refactorizar si necesario
# Convertir enums para union types, etc.

# 6. Actualizar CI/CD
# Añadir Node.js 22 en los workflows

Si estás emocionado con las novedades de TypeScript y Node.js, recomiendo revisar otro artículo: Create React App Deprecado: Migrando para Vite y el Futuro de React donde descubrirás otro gran cambio en el ecosistema JavaScript en 2025.

¡Vamos a por ello! 🦅

Comentarios (0)

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

Añadir comentarios