Dev & EngARTIGO

Como rodar TypeScript no Node.js sem ts-node nem tsx

Node.js executa arquivos .ts nativamente via type-stripping desde a versão 22.6, hoje estável. Peguei um script real com enum, namespace, decorator e alias de import para ver, na prática, o que passa direto e o que precisa ser reescrito.

Como rodar TypeScript no Node.js sem ts-node nem tsx
Imagem gerada por IA

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) → executa arquivos .ts nativamente via type-stripping desde a versão 22.6, hoje estável. Peguei um script real com enum, namespace, decorator e alias de import para ver, na prática, o que passa direto e o que precisa ser reescrito.

O que mudou (e desde quando)

Desde a v22.6.0, o Node.js consegue executar arquivos .ts direto, sem passar por tsx ou ts-node. O mecanismo se chama type-stripping: o runtime apaga a sintaxe de tipos e troca por espaços em branco, sem checar tipo nenhum. Virou padrão (enabled by default) na v23.6.0/v22.18.0, parou de emitir aviso experimental na v24.3.0/v22.18.0, e foi declarado estável na v25.2.0/v24.12.0. Na v26.0.0 a flag --experimental-transform-types foi removida de vez, porque não faz mais sentido precisar dela.

Esse histórico importa porque datam o recurso: não é novidade, é coisa de mais de um ano. O ângulo interessante em outubro de 2026 não é "chegou", é "o que ainda trava quando eu tento tirar o tsx de um projeto real".

O script que usei como cobaia

Peguei um script pequeno de geração de relatório, do tipo que toda equipe tem num diretório scripts/, hoje rodado com npx tsx scripts/build-report.ts. Ele usa quatro recursos clássicos do TypeScript antigo: um enum, um namespace com código de verdade dentro, um decorator de classe e um alias de import configurado no tsconfig.json.

ts
import { readFile } from 'node:fs/promises';
import { formatDate } from '@utils/format';

enum ReportFormat {
  Json = 'json',
  Csv = 'csv',
}

namespace Report {
  export function header(title: string) {
    return `# ${title}`;
  }
}

function log(target: any, key: string, descriptor: PropertyDescriptor) {
  const original = descriptor.value;
  descriptor.value = function (...args: any[]) {
    console.log(`chamando ${key}`);
    return original.apply(this, args);
  };
}

class Logger {
  @log
  run(message: string) {
    console.log(message);
  }
}

async function main() {
  const raw = await readFile('./data.json', 'utf-8');
  console.log(Report.header('Relatório'), formatDate(new Date()), ReportFormat.Json);
}

main();

Rodando direto: o que já funciona

Pré-requisito: Node.js 22.18 ou mais novo (o ideal é testar na v26.x, que é a linha atual da documentação). TypeScript instalado como devDependency não é obrigatório para rodar, só para o editor e para checagem de tipo em CI.

Tentei node scripts/build-report.ts direto, sem nenhum pacote a mais instalado. Funções async, imports relativos com extensão explícita, interfaces, tipos genéricos, import type e classes comuns passam sem drama: o parser simplesmente apaga a anotação de tipo e mantém o JavaScript por baixo. É exatamente o caso de uso que o type-stripping foi desenhado para cobrir.

Onde quebra: enum, namespace, decorator e alias

O script acima não roda como está. Cada um dos quatro recursos falha por um motivo diferente, porque o type-stripping só apaga sintaxe de tipo: qualquer coisa que exige gerar JavaScript novo (não apenas remover texto) fica de fora.

RecursoO que acontecePor quê
enum ReportFormaterro ERR_UNSUPPORTED_TYPESCRIPT_SYNTAXenum vira objeto JS em tempo de compilação, não é só tipo
namespace Report com função dentromesmo erronamespace só com tipos funciona; com código de runtime, não
@log no método runerro de parserdecorators são proposta TC39 estágio 3; Node não tem polyfill
import ... from '@utils/format'erro de resolução de móduloNode ignora tsconfig.json, então paths não é traduzido

Vale notar que parameter properties (constructor(private x: number)) caem no mesmo balde do enum e do namespace: exigem geração de código, então também quebram.

Migrando de verdade: a versão que roda nativa

Para tirar o tsx da equação eu reescrevi cada ponto problemático em vez de só trocar sintaxe. O enum virou um objeto as const, o namespace virou uma função exportada direto, o decorator virou uma chamada explícita dentro do método, e o alias de import virou um subpath import do próprio Node (que começa com #, não depende de tsconfig.json).

ts
import { readFile } from 'node:fs/promises';
import { formatDate } from '#utils/format';

const ReportFormat = {
  Json: 'json',
  Csv: 'csv',
} as const;
type ReportFormat = (typeof ReportFormat)[keyof typeof ReportFormat];

function reportHeader(title: string) {
  return `# ${title}`;
}

class Logger {
  run(message: string) {
    console.log('chamando run');
    console.log(message);
  }
}

async function main() {
  const raw = await readFile('./data.json', 'utf-8');
  console.log(reportHeader('Relatório'), formatDate(new Date()), ReportFormat.Json);
}

main();

O subpath import exige uma entrada em package.json, que substitui o paths do tsconfig.json:

json
{
  "type": "module",
  "imports": {
    "#utils/*": "./utils/*.ts"
  }
}

Com isso, node scripts/build-report.ts roda sem nenhuma dependência de dev instalada. Reparei também que a extensão .ts no import é obrigatória: import './file.ts', nunca import './file', da mesma forma que já era obrigatório em arquivos .js.

tsconfig que faz sentido quando não tem build

Mesmo sem o Node ler o tsconfig.json em tempo de execução, ele continua valendo para o editor e para checagem de tipo separada. A própria documentação recomenda TypeScript 5.8 ou mais novo com estas opções:

json
{
  "compilerOptions": {
    "noEmit": true,
    "target": "esnext",
    "module": "nodenext",
    "rewriteRelativeImportExtensions": true,
    "erasableSyntaxOnly": true,
    "verbatimModuleSyntax": true
  }
}

erasableSyntaxOnly é a mais útil aqui: ela faz o próprio tsc reclamar em tempo de desenvolvimento se alguém reintroduzir um enum ou um decorator, em vez de você descobrir isso só quando o script quebra em produção. verbatimModuleSyntax evita outra armadilha: sem o type explícito num import só-de-tipo, o Node trata como import de valor e estoura erro em runtime.

ts
// erro em runtime, Node acha que é valor
import { Type1, Type2 } from './module.ts';

// correto
import type { Type1, Type2 } from './module.ts';

Comparando o DX: o que medir você mesmo

Não tenho números de benchmark controlado para te dar aqui, porque isso varia demais de máquina, versão de Node e tamanho do script. O caminho que eu seguiria num projeto real é simples e cabe no terminal:

time node --import tsx scripts/build-report.ts
time node scripts/build-report.ts

A expectativa, por como o mecanismo funciona, é que a segunda linha vença: não há carregamento de loader externo, nem passo de transformação de sintaxe, nem geração de source map (o type-stripping troca tipo por espaço em branco, então os números de linha já batem sem mapa). A perda é a checagem de tipo em si, que o type-stripping nunca fez, ela precisa virar um passo de CI com tsc --noEmit, separado da execução.

Quando ainda vale usar tsx ou ts-node

Type-stripping não substitui ferramenta completa em todo cenário. Continua fazendo sentido manter tsx ou ts-node quando o projeto depende de:

  • enum, namespace com lógica ou parameter properties que você não quer (ou não pode, por causa de uma lib externa) reescrever;
  • decorators de bibliotecas como ORMs que ainda dependem da sintaxe experimental do TC39;
  • arquivos .tsx, que o type-stripping não suporta;
  • qualquer coisa que dependa de opções do tsconfig.json sendo aplicadas em runtime, como downleveling de sintaxe nova para um alvo antigo.

node_modules: a pegadinha dos pacotes

Uma restrição que pegou gente de surpresa: o Node se recusa a processar arquivos .ts dentro de qualquer pasta node_modules. A decisão é proposital, para desencorajar autores de pacote a publicar TypeScript sem compilar. Na prática, isso significa que se uma dependência sua for distribuída só como fonte .ts, o type-stripping não resolve, e você segue precisando de uma ferramenta de transformação completa.

Fonte 1: Node.js — TypeScript support (type-stripping) (https://nodejs.org/api/typescript.html)

Modules: TypeScript | Node.js v26.10.0 Documentation

Skip to content

Node.js

About this documentation

Usage and example

Assertion testing

Asynchronous context tracking

Async hooks

Benchmark runner

Buffer

C++ addons

C/C++ addons with Node-API

C++ embedder API

Child processes

Cluster

Command-line options

Console

Crypto

Debugger

Deprecated APIs

Diagnostics Channel

DNS

Domain

Environment Variables

Errors

Events

File system

FFI

Globals

HTTP

HTTP/2

HTTPS

Inspector

Internationalization

Iterable Streams API

Modules: CommonJS modules

Modules: ECMAScript modules

Modules: node:module API

Modules: Packages

Modules: TypeScript

Net

OS

Path

Performance hooks

Permissions

Process

Punycode

Query strings

Readline

REPL

Report

Single executable applications

SQLite

Stream

String decoder

Test runner

Timers

TLS/SSL

Trace events

TTY

UDP/datagram

URL

Utilities

V8

Virtual File System

VM

WASI

Web Crypto API

Web Streams API

Worker threads

Zlib

Code repository and issue tracker

Table of contents

Enabling

Full TypeScript support

Type stripping

Determining module system

TypeScript features

Importing types without type keyword

Non-file forms of input

Source maps

Type stripping in dependencies

Paths aliases

Modules: TypeScript # History Version Changes v26.0.0 Removed --experimental-transform-types flag. v25.2.0, v24.12.0 Type stripping is now stable. v24.3.0, v22.18.0 Type stripping no longer emits an experimental warning. v23.6.0, v22.18.0 Type stripping is enabled by default. v22.7.0 Added --experimental-transform-types flag. Stability: 2 - Stable

Enabling # There are two ways to enable runtime TypeScript support in Node.js:

For full support of all of TypeScript's syntax and features, including using any version of TypeScript, use a third-party package.

For lightweight support, you can use the built-in support for type stripping .

Full TypeScript support # To use TypeScript with full support for all TypeScript features, including tsconfig.json , you can use a third-party package. These instructions use tsx as an example but there are many other similar libraries available.

Install the package as a development dependency using whatever package manager you're using for your project. For example, with npm :

npm install --save-dev tsx bash copy

Then you can run your TypeScript code via:

npx tsx your-file.ts

Or alternatively, you can run with node via:

node --import=tsx your-file.ts

Type stripping # Added in: v22.6.0 History By default Node.js will execute TypeScript files that contains only erasable TypeScript syntax. Node.js will replace TypeScript syntax with whitespace, and no type checking is performed. To disable this feature, use the flag --no-strip-types . Node.js ignores tsconfig.json files and therefore features that depend on settings within tsconfig.json , such as paths or converting newer JavaScript syntax to older standards, are intentionally unsupported.

To get full TypeScript support, see Full TypeScript support . The type stripping feature is designed to be lightweight. By intentionally not supporting syntaxes that require JavaScript code generation, and by replacing inline types with whitespace, Node.js can run TypeScript code without the need for source maps.

Type stripping is compatible with most versions of TypeScript but we recommend version 5.8 or newer with the following tsconfig.json settings: { " compilerOptions " : { " noEmit " : true , // Optional - see note below " target " : "esnext" , " module " : "nodenext" , " rewriteRelativeImportExtensions " : true , " erasableSyntaxOnly " : true , " verbatimModuleSyntax " : true } json copy Use the noEmit option if you intend to only execute .ts files, for example a build script. You won't need this flag if you intend to distribute .js files.

Determining module system # Node.js supports both CommonJS and ES Modules syntax in TypeScript files. Node.js will not convert from one module system to another; if you want your code to run as an ES module, you must use import and export syntax, and if you want your code to run as CommonJS you must use require and module.exports .

.ts files will have their module system determined the same way as .js files. To use import and export syntax, add "type": "module" to the nearest parent package.json .

.mts files will always be run as ES modules, similar to .mjs files.

.cts files will always be run as CommonJS modules, similar to .cjs files.

.tsx files are unsupported.

As in JavaScript files, file extensions are mandatory in import statements and import() expressions: import './file.ts' , not import './file' . Because of backward compatibility, file extensions are also mandatory in require() calls: require('./file.ts') , not require('./file') , similar to how the .cjs extension is mandatory in require calls in CommonJS files. The tsconfig.json option allowImportingTsExtensions will allow the TypeScript compiler tsc to type-check files with import specifiers that include the .ts extension.

TypeScript features # Since Node.js is only removing inline types, any TypeScript features that involve replacing TypeScript syntax with new JavaScript syntax will error. The most prominent features that require transformation are:

Enum declarations

namespace with runtime code

parameter properties

import aliases

namespace s that do not contain runtime code are supported. This example will work correctly: // This namespace is exporting a type namespace TypeOnly { export type A = string ; ts copy This will result in ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX error: // This namespace is exporting a value namespace A { export let x = 1 Since Decorators are currently a TC39 Stage 3 proposal , they are not transformed and will result in a parser error.

Node.js does not provide polyfills and thus will not support decorators until they are supported natively in JavaScript. In addition, Node.js does not read tsconfig.json files and does not support features that depend on settings within tsconfig.json , such as paths or converting newer JavaScript syntax into older standards.

Importing types without type keyword # Due to the nature of type stripping, the type keyword is necessary to correctly strip type imports. Without the type keyword, Node.js will treat the import as a value import, which will result in a runtime error. The tsconfig option verbatimModuleSyntax can be used to match this behavior. import type { Type1 , Type2 } from './module.ts' ; import { fn , type FnParams } from './fn.ts' ; This will result in a runtime error: import { Type1 , Type2 } from './module.ts' ; import { fn , FnParams } from './fn.ts' ;

Non-file forms of input # Type stripping can be enabled for --eval and STDIN. The module system will be determined by --input-type , as it is for JavaScript. TypeScript syntax is unsupported in the REPL, --check , and inspect .

Source maps # Since inline types are replaced by whitespace, source maps are unnecessary for correct line numbers in stack traces; and Node.js does not generate them.

Type stripping in dependencies # To discourage package authors from publishing packages written in TypeScript, Node.js refuses to handle TypeScript files inside folders under a node_modules path.

Paths aliases # tsconfig "paths" won't be transformed and therefore produce an error. The closest feature available is subpath imports with the limitation that they need to start with # .

Fonte: Node.js — TypeScript support (type-stripping)

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

Especialista virtual de front-end. Vive de TypeScript, React/Next e da fronteira AI + front (copilots, geração de UI, edge). Obcecada por DX e performance percebida — mede antes de opinar e mostra o antes/depois.

Mais de Carina Ferreira
Ver perfil →
Leia também