Voltar para o Blog

TypeScript Nativo no Node.js: A Revolução do --experimental-strip-types

Olá HaWkers, 2025 marcou o início de uma nova era para desenvolvedores TypeScript: executar código TypeScript diretamente no Node.js, sem transpilação prévia.

Você já se perguntou por que precisamos compilar TypeScript para JavaScript antes de executar? E se o Node.js pudesse simplesmente... ignorar os tipos e executar direto?

A Grande Mudança no Node.js 22+

Com o Node.js 22, uma funcionalidade experimental revolucionária foi introduzida: --experimental-strip-types. Esta flag permite ao Node.js executar arquivos TypeScript nativamente, removendo anotações de tipo em tempo de execução.

O Problema Tradicional

Historicamente, trabalhar com TypeScript no Node.js sempre envolveu múltiplas etapas:

# Fluxo tradicional (ainda comum em 2024)
# 1. Escrever TypeScript
# 2. Compilar com tsc
npx tsc

# 3. Executar o JavaScript gerado
node dist/index.js

# Ou usar ferramentas como ts-node
npx ts-node src/index.ts

Ferramentas como ts-node, tsx, e ts-node-dev surgiram para simplificar, mas ainda dependiam de transpilação em memória.

Como Funciona o --experimental-strip-types

O Node.js 22+ introduziu uma abordagem radicalmente diferente: em vez de transpilar TypeScript para JavaScript, ele simplesmente remove as anotações de tipo e executa o código.

Exemplo Prático

// 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 e anterior):

# Precisava compilar primeiro
npx tsc user.ts
node user.js

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

Agora (Node.js 22+):

# Execução direta!
node --experimental-strip-types user.ts

O Que Está Acontecendo Por Baixo

O Node.js não está compilando TypeScript. Ele está fazendo algo mais simples e rápido:

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

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

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

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

Configuração Moderna para TypeScript + Node.js em 2025

Vamos criar um projeto moderno aproveitando estas novas capacidades:

1. Inicialização do Projeto

# Criar diretório
mkdir modern-node-ts
cd modern-node-ts

# Inicializar package.json
npm init -y

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

# Criar tsconfig.json
npx tsc --init

2. Configuração do tsconfig.json

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

    // Para type checking apenas, não gera output
    "noEmit": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}

3. Package.json com 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. Exemplo de Aplicação 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();

// Criar alguns usuários de exemplo
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 simples
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');
});

Agora execute:

# Desenvolvimento com hot reload
npm run dev

# Type checking
npm run check

# Produção
npm start

Vantagens do Approach Nativo

1. Startup Mais Rápido

Sem transpilação significa inicialização mais rápida, especialmente importante para:

  • Scripts CLI
  • Funções serverless
  • Testes unitários
  • Desenvolvimento local
# Comparação de tempo de startup
# ts-node (transpilação JIT)
time ts-node src/index.ts
# ~450ms

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

2. Setup Minimalista

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

Não precisa de: ts-node, tsx, @swc/core, esbuild, etc.

3. Watch Mode Nativo

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

Detecta mudanças e reinicia automaticamente, sem ferramentas externas.

4. Debugging Direto

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

Limitações e Considerações

O Que NÃO Funciona (Ainda)

1. Transformações de TypeScript

// ❌ Enums não funcionam (precisam transformação)
enum Status {
  Active = 'ACTIVE',
  Inactive = 'INACTIVE'
}

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

// ❌ Decorators não funcionam
class UserController {
  @Get('/users')
  getUsers() {}
}

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

2. Namespace Syntax

// ❌ Namespaces não são suportados
namespace Utils {
  export function formatDate() {}
}

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

3. Parâmetros Opcionais em Interfaces Implementadas

Funciona, mas tenha cuidado com compatibilidade.

Quando Ainda Usar Build Tools

Você ainda precisa de ferramentas como esbuild, swc, ou tsc para:

  • Produção com bundling
  • Compatibilidade com Node.js antigo
  • Uso de features TypeScript avançadas (enums, decorators)
  • Otimizações 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"
  }
}

Comparação: Strip Types vs Alternativas

Node.js Strip Types

# Pros:
# - Zero dependencies extras
# - Startup rápido
# - Watch mode nativo
# - Debugging direto

# Cons:
# - Experimental (pode mudar)
# - Sem transformações (enums, decorators)
# - Requer Node.js 22+

tsx (Sucessor do ts-node)

npm install -D tsx

# Pros:
# - Muito rápido (usa esbuild)
# - Suporta todas features TS
# - Funciona em Node.js antigo
# - Estável e maduro

# Cons:
# - Dependência extra
# - Transpilação JIT (overhead pequeno)

Bun

# Pros:
# - TypeScript nativo sem flags
# - Extremamente rápido
# - Runtime completo

# Cons:
# - Runtime diferente do Node.js
# - Compatibilidade pode variar

Deno

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

# Cons:
# - Ecossistema diferente
# - Requer mudanças no código

O Futuro do TypeScript no Node.js

Roadmap Esperado

2025-2026:

  • Strip types sai do experimental
  • Suporte a mais syntax TypeScript
  • Performance otimizada

2027+:

  • TypeScript como citizen de primeira classe
  • Possível suporte a type checking em runtime (opt-in)
  • Integração completa com tooling do Node.js
// Visão do 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'
  }
};

Adoção da Indústria

TypeScript está se tornando o padrão:

  • 38.5% dos desenvolvedores já usam TypeScript
  • Top 5 linguagens mais populares
  • Maioria dos novos projetos Node.js começam com TS
// Padrão emergente em 2025
// Projetos começam direto em 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"
  }
}

Migração de Projeto Existente

Passo a Passo

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

# 2. Atualizar package.json
# Adicionar script de dev

# 3. Testar execução direta
node --experimental-strip-types src/index.ts

# 4. Verificar features incompatíveis
# - Procurar por enums
# - Procurar por namespaces
# - Procurar por decorators

# 5. Refatorar se necessário
# Converter enums para union types, etc.

# 6. Atualizar CI/CD
# Adicionar Node.js 22 nos workflows

Script de Migração

// scripts/check-compatibility.ts
import { readdir, readFile } from 'fs/promises';
import { join } from 'path';

interface CompatibilityIssue {
  file: string;
  line: number;
  issue: string;
  suggestion: string;
}

async function checkFile(filePath: string): Promise<CompatibilityIssue[]> {
  const content = await readFile(filePath, 'utf-8');
  const lines = content.split('\n');
  const issues: CompatibilityIssue[] = [];

  lines.forEach((line, index) => {
    // Check for enums
    if (/^\s*enum\s+/.test(line)) {
      issues.push({
        file: filePath,
        line: index + 1,
        issue: 'Enum found',
        suggestion: 'Convert to union type or const object'
      });
    }

    // Check for namespaces
    if (/^\s*namespace\s+/.test(line)) {
      issues.push({
        file: filePath,
        line: index + 1,
        issue: 'Namespace found',
        suggestion: 'Convert to ES6 modules'
      });
    }

    // Check for decorators
    if (/^\s*@\w+/.test(line)) {
      issues.push({
        file: filePath,
        line: index + 1,
        issue: 'Decorator found',
        suggestion: 'Use alternative pattern or keep build step'
      });
    }
  });

  return issues;
}

async function scanDirectory(dir: string): Promise<CompatibilityIssue[]> {
  const entries = await readdir(dir, { withFileTypes: true });
  const issues: CompatibilityIssue[] = [];

  for (const entry of entries) {
    const fullPath = join(dir, entry.name);

    if (entry.isDirectory() && entry.name !== 'node_modules') {
      issues.push(...await scanDirectory(fullPath));
    } else if (entry.isFile() && /\.ts$/.test(entry.name)) {
      issues.push(...await checkFile(fullPath));
    }
  }

  return issues;
}

// Execute
const issues = await scanDirectory('./src');

if (issues.length === 0) {
  console.log('✅ No compatibility issues found!');
  console.log('Your project is ready for --experimental-strip-types');
} else {
  console.log(`⚠️  Found ${issues.length} compatibility issues:\n`);
  issues.forEach(issue => {
    console.log(`${issue.file}:${issue.line}`);
    console.log(`  Issue: ${issue.issue}`);
    console.log(`  Suggestion: ${issue.suggestion}\n`);
  });
}

Se você está empolgado com as novidades do TypeScript e Node.js, recomendo dar uma olhada em outro artigo: Create React App Depreciado: Migrando para Vite e o Futuro do React onde você vai descobrir outra grande mudança no ecossistema JavaScript em 2025.

Bora pra cima! 🦅

Comentários (0)

Esse artigo ainda não possui comentários 😢. Seja o primeiro! 🚀🦅

Adicionar comentário