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.tsHerramientas 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.tsAhora (Node.js 22+):
# ¡Ejecución directa!
node --experimental-strip-types user.tsQué 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 --init2. 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.tsDetecta 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 variarDeno
# Pros:
# - TypeScript first-class
# - Secure by default
# - Herramientas built-in
# Cons:
# - Ecosistema diferente
# - Requiere cambios en el códigoEl 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 workflowsSi 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.

