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.

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.
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.
| Recurso | O que acontece | Por quê |
|---|---|---|
enum ReportFormat | erro ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX | enum vira objeto JS em tempo de compilação, não é só tipo |
namespace Report com função dentro | mesmo erro | namespace só com tipos funciona; com código de runtime, não |
@log no método run | erro de parser | decorators são proposta TC39 estágio 3; Node não tem polyfill |
import ... from '@utils/format' | erro de resolução de módulo | Node 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).
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:
{
"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:
{
"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.
// 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.tsA 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.jsonsendo 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 # .
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.
Como a Vercel cortou 91% da latência de metadata no edge (e o que isso muda pra você)
A CDN da Vercel trocou o lookup de metadata por caminho por shards indexados de 200 KB. P99 caiu de 215,8 ms para 19,1 ms, e a técnica vale para quem opera qualquer edge próprio.














