Dev & EngARTIGO

Node.js roda TypeScript sem build step: o que o type-stripping nativo libera e o que ainda quebra no backend

Desde 2024 o Node.js consegue executar arquivos .ts removendo os tipos em tempo real, sem ts-node, tsx ou swc. Mas enums, namespaces com código e decorators continuam fora do alcance.

Node.js roda TypeScript sem build step: o que o type-stripping nativo libera e o que ainda quebra no backend
Imagem gerada por IA

Desde 2024 o Node.js↳Node.js45 conteúdosComo criar aplicações console em Node.jsDev (Back & Front) · fev 2025Entendendo o ciclo de publicação do Node.jsDev (Back & Front) · fev 2020Nodejs por baixo dos panosDev (Back & Front) · jul 2019Ver tudo em Dev (Back & Front) → consegue executar arquivos .ts removendo os tipos em tempo real, sem ts-node, tsx ou swc. Mas enums, namespaces com código e decorators continuam fora do alcance.

A documentação oficial do Node.js, na página Modules: TypeScript, descreve um recurso que já mudou a esteira de quem mantém serviços backend em produção: o type-stripping nativo. Ele não é novidade de hoje. A flag experimental --experimental-transform-types apareceu na v22.7.0, o recurso passou a vir ligado por padrão na v23.6.0 (e na v22.18.0, no branch LTS), deixou de emitir aviso experimental na v24.3.0/v22.18.0, virou estável na v25.2.0/v24.12.0 e, na v26.0.0, a flag experimental foi removida de vez.

O que o Node faz, de fato, com um arquivo .ts

Type-stripping não é compilação. O Node lê o arquivo .ts, localiza a sintaxe de tipo (anotações, interfaces, as Tipo) e substitui cada trecho por espaços em branco, preservando as posições de linha e coluna. Não há checagem de tipo, não há geração de código novo e, por consequência, não há necessidade de source maps: o stack trace de um erro em produção já aponta para a linha certa do .ts original, porque o espaçamento não mudou.

Isso explica a regra central do recurso: só funciona com sintaxe erasável - aquela que pode virar espaço em branco sem alterar o comportamento do programa. Tipagem pura cumpre esse requisito. Qualquer construção que precise virar código JavaScript novo, não.

O caminho mais curto: tirar tsx ou ts-node do script de start

Para um serviço HTTP comum, sem enum nem decorator, o caminho fica assim:

ts
// server.ts
import type { IncomingMessage, ServerResponse } from 'node:http';
import { createServer } from 'node:http';

function handler(req: IncomingMessage, res: ServerResponse): void {
 res.writeHead(200, { 'Content-Type': 'application/json' });
 res.end(JSON.stringify({ ok: true }));
}

createServer(handler).listen(3000);

Com "type": "module" no package.json, rodar é node server.ts, sem instalar nada a mais. O detalhe que costuma pegar quem vem de ts-node é a extensão obrigatória: import './util.ts' funciona, import './util' não. A mesma regra vale para require('./file.ts') em projetos CommonJS (arquivos .cts).

A documentação recomenda TypeScript 5.8 ou mais recente e um tsconfig.json com erasableSyntaxOnly: true, verbatimModuleSyntax: true, module: nodenext e rewriteRelativeImportExtensions: true - mas isso é só para o editor e para o tsc usado como checador de tipos em paralelo, nunca em tempo de execução. O Node ignora o tsconfig.json por completo.

O que ainda obriga a manter um transpiler no CI

A própria documentação lista as construções que geram ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX, porque todas exigem gerar JavaScript novo, não apenas apagar anotação:

  • Enums (enum Cor { Vermelho, Verde }) - viram objeto em runtime, não é tipo puro.
  • Namespaces com código de runtime - um namespace A { export let x = 1 } quebra; um namespace TypeOnly { export type A = string } funciona, porque não sobra nada em tempo de execução.
  • Parameter properties (constructor(private nome: string) {}) - o açúcar sintático que cria o campo da classe a partir do parâmetro do construtor.
  • Import aliases no estilo import A = require('mod').
  • Decorators - ainda proposta Stage 3 no TC39; o Node não faz polyfill e trata como erro de parser até virarem JavaScript padrão.

Quem usa NestJS, TypeORM com decorators ou qualquer framework apoiado em @Injectable()/@Entity() não tem como rodar direto com type-stripping hoje. O mesmo vale para bases de código legadas cheias de enum - trocar por as const com union type resolve, mas é refatoração, não configuração.

Importação de tipo exige a palavra type, sem exceção

Como o stripping é cego ao significado - só olha sintaxe -, a diferença entre importar um tipo e importar um valor precisa estar explícita no código:

ts
import type { Usuario } from './modelos.ts'; // ok: vira espaço em branco
import { Usuario } from './modelos.ts'; // erro em runtime se Usuario for só tipo

Sem o type explícito, o Node trata o import como valor e o módulo quebra ao tentar resolver algo que não existe no arquivo compilado. A opção verbatimModuleSyntax do tsconfig.json ajuda o tsc a cobrar essa disciplina no editor, antes de chegar ao runtime.

paths do tsconfig não existe para o Node

Quem depende de alias de caminho (@app/* apontando para src/app/*) via paths do tsconfig.json perde esse recurso: o Node não lê o arquivo e gera erro ao encontrar o import. A alternativa oficial são os subpath imports do package.json (campo imports), com a limitação de que a chave precisa começar com # - um mecanismo mais limitado e menos familiar do que paths, mas nativo e sem transpiler.

Pacotes de terceiros continuam fora

Para desencorajar bibliotecas publicadas em TypeScript puro, o Node se recusa a aplicar type-stripping em arquivos .ts dentro de qualquer pasta sob node_modules. Isso significa que o recurso resolve o código da sua aplicação, não a cadeia de dependências - quem publica pacote ainda precisa compilar para JavaScript antes de subir ao npm, como sempre foi.

Onde isso efetivamente substitui ts-node, tsx e swc-node

CenárioPrecisa de transpiler?
Script CLI simples, tipos e interfaces apenasNão
Serviço HTTP sem enum/decorator, import type disciplinadoNão
Projeto com NestJS, TypeORM ou qualquer @Decorator()Sim
Base de código com enum espalhadoSim (ou refatorar para as const)
Alias de import via paths do tsconfigSim, ou migrar para subpath imports com #
Arquivo .tsx (JSX + TypeScript)Sim - .tsx não é suportado pelo type-stripping

Em resumo: para backend puro - sem JSX, sem decorator, sem enum -, dá para tirar tsx ou ts-node do script de start e do Dockerfile, reduzindo uma dependência de build inteira. Para o resto, inclusive qualquer front-end em .tsx, o transpiler continua no caminho, e a estratégia mais sã costuma ser migrar primeiro as partes erasáveis e manter o restante no fluxo de build que já existe.

Vale lembrar que o suporte cobre --eval e entrada via STDIN, mas não cobre REPL, --check nem node inspect - detalhe que pega quem tenta depurar um trecho de TypeScript direto no terminal interativo e recebe erro de sintaxe sem entender por quê.

Fonte: Documentação oficial do Node.js, Modules: TypeScript.

Fonte: Documentação oficial do Node.js — TypeScript (Modules: TypeScript)

Este artigo foi escrito por Bisneto Braga, colunista de back-end. Conteúdo produzido por agente de IA da redação iMasters, sob revisão editorial humana. Saiba como produzimos no expediente.

Bisneto BragaColunista

Especialista virtual de back-end, arquétipo staff engineer/consultor poliglota: já manteve monolito PHP, app Rails e serviço Java em produção. Lema declarado na bio: linguagem é ferramenta, contexto é rei. Sem torcida — a opinião dele é sempre comparativa e pragmática.

Mais de Bisneto Braga
Ver perfil →
Leia também