Back

TypeScript nativo no Node.js: O guia completo para executar arquivos .ts sem compilador

Por mais de uma década, executar TypeScript no Node.js significava uma coisa: compilar primeiro, executar depois. Seja usando tsc, ts-node, tsx, esbuild ou swc, sempre havia um passo intermediário entre seus arquivos .ts e a execução real. Essa era está acabando.

A partir do Node.js 22 e agora estável no Node.js 25, você pode executar arquivos TypeScript diretamente:

node app.ts

Sem etapa de build. Sem tsconfig.json obrigatório. Sem dependências externas. Apenas Node.js e seu código TypeScript.

Isso não é uma feature experimental ou de nicho. É uma mudança fundamental no ecossistema Node.js que afeta todo desenvolvedor TypeScript. Neste guia, vamos cobrir exatamente como funciona, o que pode e não pode fazer, e como adotá-lo em projetos reais.

Como funciona: Type Stripping, não Type Checking

O insight por trás da abordagem do Node.js é radical na sua simplicidade: remover os tipos, executar o JavaScript que sobra.

O Node.js não compila TypeScript. Não verifica tipos no seu código. Não transforma sua sintaxe. Ele literalmente remove as anotações de tipo e executa o que resta como JavaScript normal.

Considere este arquivo 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));

O Node.js transforma isso em aproximadamente:

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

A declaração interface User? Desaparece. A anotação de tipo : User? Removida. O tipo de retorno : string? Eliminado. O que resta é JavaScript perfeitamente válido que a V8 pode executar imediatamente.

Isso é chamado de "sintaxe apagável" (erasable syntax) — sintaxe TypeScript que pode ser removida sem mudar o comportamento em tempo de execução do código. E cobre a grande maioria das funcionalidades TypeScript que os desenvolvedores usam diariamente.

A implementação: Amaro e SWC

Por baixo dos panos, o Node.js usa uma biblioteca chamada Amaro para realizar o type stripping. Amaro é um wrapper fino em torno do @swc/wasm-typescript — o build WebAssembly do parser TypeScript do SWC.

O pipeline fica assim:

seu-arquivo.ts → Amaro (SWC WASM) → JS sem tipos → execução V8

Essa arquitetura tem várias implicações importantes:

  1. É rápido. O SWC é escrito em Rust e compilado para WebAssembly. A operação de type stripping é ordens de magnitude mais rápida que uma compilação completa do tsc porque só faz parse e remove — não resolve tipos, não verifica restrições nem emite arquivos de declaração.

  2. Já vem embutido. O Amaro é distribuído junto com o Node.js. Não precisa de npm install. Não polui o node_modules. Faz parte do runtime.

  3. É limitado por design. Como usa o SWC para stripping (não compilação completa), apenas sintaxe TypeScript apagável é suportada no modo padrão.

A linha do tempo: de experimental a estável

A jornada até o TypeScript nativo foi sistemática:

VersãoMarco
Node.js 22.6.0 (Julho 2024)Flag --experimental-strip-types introduzida
Node.js 22.7.0 (Agosto 2024)--experimental-transform-types adicionado para enums
Node.js 23.x (Final de 2024)Refinamentos, testes extensivos
Node.js 22.18.0 / 23.6.0 (2025)Type stripping habilitado por padrão (sem flag)
Node.js 25.2.0 (2026)Release estável, warnings experimentais removidos

No início de 2026, se você está rodando Node.js 22.18+ ou qualquer versão recente, o type stripping funciona out of the box. Sem flags, sem configuração.

O que funciona: Sintaxe TypeScript apagável

As seguintes funcionalidades do TypeScript funcionam perfeitamente com o type stripping do Node.js porque podem ser removidas sem afetar o comportamento em runtime:

Anotações de tipo

// Tudo isso simplesmente é removido const name: string = 'hello'; function add(a: number, b: number): number { return a + b; } const items: Array<string> = ['a', 'b', 'c'];

Interfaces e alias de tipo

interface Config { host: string; port: number; debug?: boolean; } type DatabaseURL = `postgres://${string}`; // São completamente apagados — não existem em 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 e casts com 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 utilitários

type ReadonlyUser = Readonly<User>; type PartialConfig = Partial<Config>; type UserKeys = keyof User; // Tudo apagado no momento do type stripping

O que não funciona: Sintaxe não apagável

Aqui as coisas ficam mais complicadas. Algumas funcionalidades do TypeScript produzem código em runtime — não podem simplesmente ser removidas porque isso mudaria o comportamento. Essas são chamadas de funcionalidades "não apagáveis".

Enums (clássicos)

// ❌ Erro em runtime apenas com type stripping enum Direction { Up = 'UP', Down = 'DOWN', Left = 'LEFT', Right = 'RIGHT', }

Enums geram objetos JavaScript em runtime. Remover a palavra-chave enum deixaria sintaxe inválida. Para usar enums, você precisa da flag --experimental-transform-types:

node --experimental-transform-types app.ts

Alternativa melhor: Use objetos as const ao invés de enums:

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

Propriedades de parâmetros

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

Propriedades de parâmetros (public, private, protected, readonly em parâmetros do construtor) geram código de atribuição. O atalho public name: string se torna this.name = name; no corpo do construtor. Essa transformação vai além do stripping.

Alternativa:

// ✅ Funciona com type stripping normal class User { name: string; // Anotação de tipo — removida private _age: number; constructor(name: string, age: number) { this.name = name; this._age = age; } }

Decorators legados e emitDecoratorMetadata

// ❌ Não suportado — geram metadata em runtime @Controller('/users') class UserController { @Get('/:id') getUser(@Param('id') id: string) { ... } }

Decorators legados (experimentais) e emitDecoratorMetadata emitem metadata de reflexão em runtime. Isso é comum em frameworks como NestJS, TypeORM e Angular.

Nota: Os decorators TC39 Stage 3 (o padrão moderno) são uma funcionalidade JavaScript e são tratados separadamente pelo motor JavaScript do Node.js.

Namespaces com merge em runtime

// ❌ Não suportado — namespaces geram IIFEs namespace Validation { export function isEmail(str: string): boolean { return str.includes('@'); } }

Alternativa: Use módulos ES:

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

Restrições importantes e armadilhas

Sem verificação de tipos

A coisa mais importante para entender: o Node.js não verifica os tipos do seu código. Se você tem um erro de tipo:

const name: number = "hello"; // Erro de tipo!

O Node.js vai tranquilamente remover a anotação : number e executar o código. Ele não tem ideia (e nenhum interesse) se os tipos estão corretos. Isso continua sendo trabalho do tsc.

Seu workflow vira:

# Desenvolvimento: só execute node app.ts # CI/CD: verificação de tipos separada npx tsc --noEmit

Na verdade, isso é uma melhoria significativa de velocidade. Durante o desenvolvimento, você pula completamente a verificação de tipos e tem execução instantânea. A verificação de tipos acontece no seu editor (via TypeScript language server) e no CI.

Especificadores de import: .ts vs .js

Esse é um dos problemas mais complicados na migração. O Node.js requer extensões de arquivo explícitas nos imports quando usando ES modules:

// ❌ Ambíguo — Node.js não sabe qual arquivo você quer import { greet } from './utils'; // ✅ Extensão .ts explícita import { greet } from './utils.ts';

A solução: O TypeScript 5.7+ introduziu a opção rewriteRelativeImportExtensions:

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

Isso diz ao tsc para aceitar especificadores de import com .ts e reescrevê-los para .js na saída.

Path aliases do tsconfig.json não funcionam

// ❌ Node.js não lê tsconfig.json import { db } from '@/database';

O Node.js não lê nem processa tsconfig.json. Path aliases são uma funcionalidade de compile-time. As alternativas incluem:

  1. Subpath imports do Node.js no package.json:
{ "imports": { "#src/*": "./src/*" } }
import { db } from '#src/database.ts';
  1. Imports relativos (a abordagem mais simples na maioria dos casos)

Migração real: de ts-node para nativo

Antes: setup com 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" } }

Depois: 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" } }

Observe o que mudou:

  1. ts-node sumiu. Removido completamente das devDependencies.
  2. O script dev é simplesmente node. Combinado com --watch para modo watch embutido.
  3. start executa .ts diretamente. Sem necessidade de compilar para dist/ no desenvolvimento.
  4. typecheck está separado. Verificação de tipos se torna uma etapa explícita e opcional.
  5. build ainda usa tsc. Para builds de produção onde você precisa de arquivos de declaração ou mirar em runtimes mais antigos.

Passos de migração

  1. Atualize o Node.js para 22.18+ (ou 25+ para estável sem warnings).

  2. Remova ts-node e tsx:

npm uninstall ts-node tsx
  1. Atualize as extensões de import para .ts:
// Antes import { db } from './database.js'; // Depois import { db } from './database.ts';
  1. Substitua enums por objetos as const (ou use --experimental-transform-types).

  2. Habilite erasableSyntaxOnly no tsconfig (TypeScript 5.8+):

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

Essa flag instrui o tsc a rejeitar qualquer sintaxe não apagável (enums, propriedades de parâmetros, namespaces) em tempo de compilação, garantindo que seu código sempre seja compatível com o type stripping do Node.js. Se você acidentalmente usar uma feature que o Node.js não consegue processar, o TypeScript vai pegar antes do runtime.

  1. Atualize os scripts no package.json.

  2. Adicione typecheck ao CI:

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

Você deveria adotar isso hoje?

Aqui está uma matriz de decisão:

CenárioRecomendação
Projeto novo, Node.js moderno✅ Use TypeScript nativo
Ferramentas CLI e scripts✅ Caso de uso perfeito — zero etapa de build
Desenvolvimento/prototipagem✅ Iteração mais rápida possível
Servidor API (Express, Fastify)✅ Funciona muito bem, adicione tsc no CI
NestJS / TypeORM (decorators)⚠️ Espere — decorators legados não suportados
Biblioteca com arquivos de declaração⚠️ Ainda precisa do tsc para gerar .d.ts
Builds de produção para Node.js antigo❌ Mantenha tsc para compilação

O workflow híbrido recomendado

Para a maioria dos projetos em 2026, o workflow ideal é:

Desenvolvimento:  node --watch app.ts     (instantâneo, sem build)
Editor:           TypeScript LSP          (verificação de tipos em tempo real)
CI:               tsc --noEmit            (verificação rigorosa de tipos)
Produção:         node app.ts             (execução direta)

Impacto no ecossistema

O suporte nativo a TypeScript no Node.js tem efeitos cascata em todo o ecossistema:

Ferramentas que se tornam menos necessárias:

  • ts-node — a substituição mais direta; amplamente desnecessário para desenvolvimento
  • tsx — a alternativa mais rápida ao ts-node; mesma situação
  • esbuild / swc como transpiladores de desenvolvimento — o Node.js agora cuida disso

Ferramentas ainda essenciais:

  • tsc — para verificação de tipos, geração de arquivos de declaração e mirar em ambientes mais antigos
  • Bundlers (Vite, webpack, Rollup) — para código do navegador, tree-shaking e otimização
  • @swc/core / esbuild — para pipelines de build em produção onde você precisa de transformação completa

Frameworks se adaptando:

  • Deno e Bun já tinham suporte nativo a TypeScript. O Node.js entrando no clube nivelou o campo de jogo.
  • NestJS está explorando decorators TC39 (que não precisam de transformação de tipos) como caminho de migração.
  • Express e Fastify funcionam perfeitamente com type stripping nativo — sem necessidade de mudanças.

Olhando pra frente

O suporte nativo a TypeScript no Node.js reflete uma tendência maior: a linha entre TypeScript e JavaScript tá ficando cada vez mais borrada. TypeScript não é mais uma linguagem de "compilar pra JS" no sentido clássico. Está virando um dialeto de JavaScript que os runtimes entendem nativamente.

O time do TypeScript tá acelerando essa convergência. TypeScript 5.8 trouxe a flag --erasableSyntaxOnly pra forçar compatibilidade com Node.js em tempo de compilação. TypeScript 6.0 Beta saiu em fevereiro de 2026 como uma release de transição. E a mudança mais ambiciosa é o TypeScript 7.0 (Project Corsa): uma reescrita completa do compilador e serviço de linguagem em Go, mirando 10x de melhoria de performance. Com previsão pra meados de 2026, o Project Corsa deixaria o tsc --noEmit tão rápido que o argumento de "pular type checking no dev" pode virar irrelevante.

Enquanto isso, a proposta TC39 Type Annotations (Stage 1) quer tornar anotações de tipo parte da spec do JavaScript. Se avançar, navegadores e runtimes vão ignorar anotações de tipo nativamente, exatamente o que o Node.js já faz hoje com o Amaro.

Estamos caminhando para um mundo onde a pergunta não é "Como eu compilo TypeScript?" mas sim "Por que eu precisaria compilar?"

Para a maioria das aplicações, a resposta é cada vez mais: você não precisa.

Conclusão

O suporte nativo a TypeScript no Node.js não é só uma funcionalidade de conveniência. É uma mudança de paradigma. O ciclo de desenvolvimento encolhe de editar → compilar → executar pra editar → executar. A árvore de dependências fica mais leve. Aquela dor de cabeça com configuração de build some.

Se você tá começando um novo projeto Node.js em 2026, não tem razão pra montar um pipeline de compilação TypeScript pro desenvolvimento. Escreva arquivos .ts e execute com node. Deixe o tsc pra verificação de tipos no CI e pros casos onde você precisa de arquivos de declaração ou saída compilada.

O futuro do TypeScript no Node.js não é sobre compiladores melhores. É sobre não precisar de um.

Node.jsTypeScriptjavascriptnodejsbackenddeveloper-toolsbuild-tools

Explore ferramentas relacionadas

Experimente estas ferramentas gratuitas do Pockit