@d1g1tal/tsnode
v1.2.0
Published
ESM-only Node.js TypeScript runner — run .ts files directly with `tsnode foo.ts`. CommonJS support removed.
Maintainers
Readme
tsnode
ESM-only Node.js TypeScript runner.
tsnode foo.tsForked from tsx by Hiroki Osame. This fork removes legacy dual-mode branching and focuses on a fast ESM-only path.
Table of contents
- What tsnode is
- How tsnode works
- Install
- Quick start
- How to choose between tsnode and node --import
- CLI argument rules
- Run a TypeScript file
- Pass script arguments
- Eval code (-e)
- Print expression result (-p)
- REPL
- Watch mode
- Node test runner with TypeScript
- Custom tsconfig path
- Disable transform cache
- Shell scripts
- Fast path: node --import
- Programmatic API
- Source maps and debugging
- Type-checking and compiler behavior
- Performance and cache behavior
- ESM-only expectations
- CLI and environment reference
- Troubleshooting
- FAQ
- What was removed from tsx
- Requirements
- License and attribution
What tsnode is
tsnode runs TypeScript files directly in Node.js without requiring a separate build step first.
If you normally do this:
- write
.ts - compile to
.js - run Node on compiled output
tsnode simplifies that into one step for runtime execution.
This project is intentionally ESM-only. That focus removes old compatibility branches and keeps runtime behavior and performance easier to reason about.
How tsnode works
tsnode registers a Node loader hook. When Node requests a TypeScript module, that module is transformed on demand and executed.
Important mental model:
- It is runtime transpilation.
- It is not a bundler.
- It is not a type-checker.
- It is built for modern ESM workflows.
Most confusion around this library comes from expecting one of these:
- CommonJS runtime compatibility (
requireworkflows) - Type-checking while executing
- One single "best" command for every scenario
This README is organized to make each scenario explicit.
Install
Local project dependency (recommended)
pnpm add -D @d1g1tal/tsnode
# or
npm i -D @d1g1tal/tsnode
# or
yarn add -D @d1g1tal/tsnodeRun from your project:
pnpm tsnode ./src/main.tsGlobal install
pnpm add -g @d1g1tal/tsnode
# or
npm i -g @d1g1tal/tsnode
# or
yarn global add @d1g1tal/tsnodeThen:
tsnode ./main.tsQuick start
Use these commands as your baseline:
- Run a file
tsnode ./script.ts- Watch and rerun on change
tsnode watch ./script.ts- Fastest one-off execution path
node --import @d1g1tal/tsnode ./script.ts- Type-check in a separate step
tsc --noEmitHow to choose between tsnode and node --import
This is the decision that matters most in day-to-day use.
Use tsnode when you want CLI features:
watch--test- REPL
-e/-p
Use node --import @d1g1tal/tsnode when startup overhead is the main concern for plain file execution.
Rule of thumb:
- Feature-rich workflow: use
tsnode - Lowest startup overhead for direct script execution: use
node --import
CLI argument rules
Argument placement is the most common source of mistakes.
General pattern:
tsnode [tsnode/node flags] ./entry.ts [script args]Example:
tsnode --tsconfig ./tsconfig.scripts.json ./scripts/sync.ts --dry-run --verboseHow it is interpreted:
--tsconfigconfigures runtime behavior./scripts/sync.tsis your script entrypoint--dry-run --verboseare received by your script viaprocess.argv
Run a TypeScript file
This is the core use case.
tsnode ./src/main.tsWhere this is useful in real projects:
- migration scripts
- release scripts
- data utilities
- internal tooling commands
Real-world example:
tsnode ./scripts/migrate.ts --environment=stagingIf your team keeps ops scripts in TypeScript, this is usually your default command.
Pass script arguments
Arguments after the script path are passed directly to your script.
tsnode ./scripts/report.ts --since=2026-01-01 --format=jsonScript example:
console.log(process.argv.slice(2));Output:
[ '--since=2026-01-01', '--format=json' ]Where this helps:
- CI pipelines with parameterized scripts
- scheduled jobs with date windows
- safety controls like
--dry-run
Eval code (-e)
-e runs a TypeScript snippet directly from the command line.
tsnode -e 'const n: number = 42; console.log(n * 2)'Use this when you need a quick experiment without creating a file.
Real-world examples:
- validating parser behavior against a sample
- quickly reproducing part of a bug
- trying a tiny data transform
Print expression result (-p)
-p evaluates an expression and prints its result.
tsnode -p 'new Date(0).toISOString()'This is ideal for one-liner checks, shell workflows, and quick normalization logic.
Real-world examples:
- date formatting checks
- quick path/string transformations
- tiny utility evaluations in terminal workflows
REPL
Running tsnode with no arguments starts an interactive TypeScript REPL.
tsnodeThis is great for trying ideas before writing files.
You still get normal Node REPL behavior:
.help.exit- tab completion
_for last result
Watch mode
Watch mode reruns your script whenever relevant files change.
tsnode watch ./src/main.tsPress Return to manually rerun.
This is useful when you are in a fast edit/run/debug loop.
Useful watch options:
| Flag | What it helps with |
|---|---|
| --include <path> | Add files outside import graph (for example config files) |
| --exclude <path> | Ignore generated or noisy files |
| --clear-screen=false | Keep previous output visible |
| --no-cache | Debug cache-sensitive behavior |
| --tsconfig <path> | Use a specific tsconfig for watch session |
Example:
tsnode watch \
--include ./config/runtime.json \
--exclude './generated/*' \
--clear-screen=false \
./src/server.tsNode test runner with TypeScript
tsnode --test enables TypeScript execution for Node's built-in test runner.
tsnode --testUse this if you already prefer node:test and want to keep your test files in TypeScript.
Pattern-based example:
tsnode --test ./tests/**/*.test.tsThis keeps your test runtime simple without adding a separate compile phase just for tests.
Custom tsconfig path
By default, tsnode finds tsconfig.json from the working directory. Use --tsconfig when that is not the config you want.
tsnode --tsconfig ./configs/tsconfig.scripts.json ./scripts/sync.tsTypical cases:
- monorepos
- separate app/tooling/test tsconfig files
- dedicated script/runtime tsconfig
With node --import, use:
TSNODE_TSCONFIG_PATH=./configs/tsconfig.scripts.json node --import @d1g1tal/tsnode ./scripts/sync.tsDisable transform cache
In normal use, cache improves repeated execution. During debugging, a cache-free run can be useful.
tsnode --no-cache ./src/main.tsUse this when:
- validating cache invalidation behavior
- investigating stale cache suspicions
- collecting deterministic no-cache timing data
Shell scripts
You can execute TypeScript files directly as shell scripts using a shebang.
#!/usr/bin/env tsnode
console.log('argv:', process.argv.slice(2));Make it executable:
chmod +x ./script.ts
./script.ts hello worldThis is especially useful for team automation scripts where TypeScript readability is preferable to complex shell script logic.
Fast path: node --import
For low-overhead direct file execution, use:
node --import @d1g1tal/tsnode ./src/main.tsWhy it is often faster:
- tsnode CLI may spawn a child process depending on mode
node --importruns in a single process
Good use cases:
- short-lived scripts called frequently
- Makefile / Docker commands that already run
node - startup-focused benchmarks
Set custom tsconfig for this mode:
TSNODE_TSCONFIG_PATH=./path/to/tsconfig.custom.json node --import @d1g1tal/tsnode ./main.tsInject through NODE_OPTIONS when another tool launches Node internally:
NODE_OPTIONS='--import @d1g1tal/tsnode' npx some-binaryCaveat: child Node processes inherit NODE_OPTIONS, which can add overhead in process-heavy workflows.
Optional helper function:
# ~/.bashrc or ~/.zshrc
tsnode() {
case "$1" in
""|watch|-e|--eval|-p|--print|--test)
command tsnode "$@"
;;
*)
node --import @d1g1tal/tsnode "$@"
;;
esac
}Programmatic API
import { register, tsImport } from '@d1g1tal/tsnode/api';Use the API when you are embedding TypeScript loading into another runtime process.
register()
register() installs loader hooks in the current process.
import { register } from '@d1g1tal/tsnode/api';
const { unregister } = register();
await import('./task.ts');
await unregister();Where this is useful:
- worker processes loading TypeScript jobs
- plugin hosts
- runtime utilities that need temporary TS loading support
Scoped namespace example:
import { register } from '@d1g1tal/tsnode/api';
const api = register({ namespace: `tenant-${Date.now()}` });
const moduleA = await api.import('./plugin.ts', import.meta.url);
await api.unregister();This helps isolate module loading between independent plugin/task runs.
Track imports with onImport:
import { register } from '@d1g1tal/tsnode/api';
register({
onImport(url) {
console.log('Loaded:', url);
}
});tsImport()
tsImport() dynamically imports a TypeScript module with targeted registration behavior.
Basic use:
import { tsImport } from '@d1g1tal/tsnode/api';
const loaded = await tsImport('./task.ts', import.meta.url);Object form:
import { tsImport } from '@d1g1tal/tsnode/api';
const loaded = await tsImport('./task.ts', {
parentURL: import.meta.url,
tsconfig: './tsconfig.tools.json',
onImport(url) {
console.log(url);
}
});Disable tsconfig lookup for a specific import:
await tsImport('./task.ts', {
parentURL: import.meta.url,
tsconfig: false
});Use this when you want dynamic TS imports without permanently changing unrelated runtime imports.
Source maps and debugging
Source maps turn stack traces and debugger locations back into TypeScript lines.
Source maps are enabled automatically when Node starts with:
--enable-source-maps- any
--inspect*flag NODE_V8_COVERAGE
Force source maps on for non-debug runs:
TSNODE_SOURCE_MAPS=1 tsnode ./src/main.tsType-checking and compiler behavior
tsnode runs code; it does not replace static type-checking.
Run type-checking separately:
tsc --noEmitRecommended workflow:
- run with tsnode for execution speed
- validate with
tsc --noEmitin CI/pre-commit
Compiler caveats inherited from esbuild:
eval()compatibility semantics are not preserved- only a subset of tsconfig options affect transforms
emitDecoratorMetadatais not supported
References:
Performance and cache behavior
This fork is optimized for fast ESM execution.
Practical guidance:
- warm runs usually benefit from cache reuse
--no-cacheis for diagnostics, not normal usenode --importis usually best for startup-sensitive one-off runs
Benchmark whichever path matches your real workload:
tsnode ./file.tsnode --import @d1g1tal/tsnode ./file.ts
ESM-only expectations
This runtime expects modern ESM usage.
In practice:
- use
import/export - do not rely on legacy CommonJS runtime patching
- align project scripts and tooling around ESM behavior
If migrating from mixed CJS/ESM code, convert runtime scripts to ESM first, then switch execution to tsnode.
CLI and environment reference
CLI flags
| Flag | Meaning |
|---|---|
| --help, -h | Show CLI help |
| --version, -v | Show tsnode version |
| --tsconfig <path> | Use a specific tsconfig |
| --no-cache | Disable transform cache |
| --test | Run Node test runner with TS support |
| --eval, -e <code> | Evaluate code |
| --print, -p <expr> | Evaluate and print expression |
Watch subcommand options:
| Flag | Meaning |
|---|---|
| watch --include <path> | Add extra watch targets |
| watch --exclude <path> | Exclude paths from watch |
| watch --clear-screen=false | Keep output between reruns |
Environment variables
| Variable | Effect |
|---|---|
| TSNODE_TSCONFIG_PATH | Set tsconfig path (especially with node --import) |
| TSNODE_SOURCE_MAPS=1 | Force source maps on |
| NODE_OPTIONS=--import @d1g1tal/tsnode | Inject loader into tools that launch Node internally |
Troubleshooting
Why are my types not being checked?
Because tsnode executes TypeScript but does not perform static type-checking. Run tsc --noEmit separately.
Why is node --import faster than tsnode for my quick script?
node --import avoids some CLI/process overhead and is often faster for short-lived runs.
Why does my CommonJS script not work?
This project is ESM-only. Convert runtime scripts to ESM module patterns.
Why are my stack traces not mapped to .ts lines?
Enable source maps explicitly:
TSNODE_SOURCE_MAPS=1 tsnode ./src/main.tsFAQ
Is this a drop-in replacement for every Node + TS setup?
It is a strong drop-in for ESM-first runtime workflows. It is not a drop-in for legacy CommonJS runtime setups.
Do I need typescript installed at runtime?
No. Runtime transforms are handled by esbuild.
Is watch mode available through node --import?
No. Watch mode is a tsnode CLI feature.
Should I always use node --import?
Use it when startup overhead is your bottleneck. For watch/test/repl/eval/print workflows, use tsnode CLI.
What was removed from tsx
- CommonJS runtime support (
requirepatching,tsx/cjsentry) - Legacy dual-mode API/extension compatibility layers
- Legacy export pre-parsing and old interop shims
package.jsontype walking for module mode detection
This fork assumes TypeScript files execute as ESM.
Requirements
- Node.js >= 24.11.1
TypeScript runtime details:
- no local
typescriptpackage is required at runtime - transforms are handled by esbuild (with native stripping where available)
- contributors to this repository use TypeScript 6.x for type-checking
License and attribution
MIT.
Forked from privatenumber/tsx, original work Copyright (c) Hiroki Osame.
See LICENSE.
