skip to content

ts-node — Run TypeScript directly in Node.js

Package-level reference for ts-node on npm — TypeScript-aware REPL, transpile-only mode, ESM loader, and its tsx-driven decline.

9 min read 25 snippets deep dive

ts-node#

What it is#

ts-node is the original “TypeScript Execute” tool for Node.js — a CLI and Node loader that compiles .ts files on-the-fly via the TypeScript compiler API, then hands them to Node. From ~2015 until ~2023 it was the default way to run TypeScript scripts and dev servers without a build step.

It is now in maintenance mode. New projects almost always pick tsx (faster, native ESM, better defaults), and Node 22+ ships native TypeScript stripping behind a flag. ts-node survives in long-running codebases, in tooling that needs the real TypeScript compiler (for paths alias resolution, decorators metadata, project references), and in the REPL niche where its TS-aware shell is still unmatched.

Install#

# Project-local with the TS compiler
npm install -D ts-node typescript
pnpm add -D ts-node typescript
yarn add -D ts-node typescript

Output: ts-node, ts-node-esm, ts-node-script, and ts-node-transpile-only binaries under node_modules/.bin/.

# One-off without installing
npx ts-node ./script.ts

Output: downloads ts-node + typescript into the npm cache, then runs the script.

# Global (not recommended; pins typescript at user level)
npm install -g ts-node typescript

Output: ts-node and typescript available system-wide.

Versioning & Node support#

  • Current line is 10.x. The previous big break was 10.0 (mid-2021), which standardised the ESM loader and registered the swc transformer option.
  • Requires Node 14.x or newer; 10.9+ works on Node 18 / 20 / 22 (though native TS stripping in 22 makes ts-node redundant for the simple case).
  • Always paired with typescript as a peer (4.x or 5.x for the 10.x series).
  • SemVer respected; minor releases mostly follow typescript major bumps.
  • Maintenance has slowed — the repo still merges PRs but rarely ships new features. Watch the changelog rather than the npm version page.

Package metadata#

Peer dependencies & extras#

CompanionRole
typescriptThe actual compiler. ts-node delegates to tsc’s API for type-aware emit.
@swc/core + @swc/wasmOptional transformer for faster transpilation (--swc flag); skips type-checking.
tsconfig-pathsResolves paths aliases at runtime — frequently needed; not bundled.
@types/node@types/node only; ts-node doesn’t depend on @types/* but most consumers do.
ts-node-devCompanion package adding fast restart-on-change. Largely superseded by tsx watch / nodemon.

Alternatives#

ToolTrade-off
tsxThe recommended successor — esbuild-based, ESM by default, watch mode built-in. 10–100× faster startup. Strips types without checking.
node —import (Node 22+)Native TypeScript stripping via --experimental-strip-types. No JSX, no decorators, no paths. Smallest dependency footprint.
bunNative TypeScript runtime — fastest, but commits you to Bun (not Node).
denoSame idea — Deno runs TS natively, separate runtime with permissions model.
swc-nodeSWC-based loader; comparable to tsx. Less polished CLI.
ts-node-devts-node + auto-restart. Use tsx watch instead in new projects.

Real-world recipes#

The remaining places ts-node still earns its keep — plus migration hints when it doesn’t.

TypeScript-aware REPL#

ts-node’s REPL is still the best interactive TS shell. tsx and Node’s native runner don’t (yet) match its inline type inference.

ts-node

Output:

>
> const x: number = 42
undefined
> x.toFixed(2)
'42.00'
> interface Pt { x: number; y: number }
undefined
> const p: Pt = { x: 1, y: 2 }
undefined
> p.x + p.y
3

The REPL respects your local tsconfig.jsonstrict, paths, lib all apply. Great for spelunking through a codebase or sketching types.

--transpile-only for fast scripts#

When you don’t need type-checking at runtime (you have tsc --noEmit in CI), skip it for ~5× faster startup:

ts-node --transpile-only ./script.ts

# Or via env
TS_NODE_TRANSPILE_ONLY=true ts-node ./script.ts

Output:

Hello, world (transpile-only — types were not checked)

This is the mode tsx operates in by default; ts-node --transpile-only is the closest equivalent without leaving the ts-node ecosystem.

Project references and paths aliases#

ts-node integrates with the TypeScript compiler API, so paths, references, and composite projects work without extra packages:

// tsconfig.json
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": { "@/*": ["src/*"] }
  },
  "ts-node": {
    "require": ["tsconfig-paths/register"]
  }
}
ts-node ./src/server.ts

Output:

Resolved @/utils → src/utils — server listening on :3000

tsx requires an explicit tsconfig-paths/register (or tsx-paths) layer; ts-node bakes it into its config block.

ESM mode#

For ESM packages ("type": "module"), use the ts-node-esm binary or pass --esm:

ts-node-esm ./script.ts
# or
node --loader ts-node/esm ./script.ts

Output:

ExperimentalWarning: --experimental-loader may be removed in the future...
top-level await ran successfully

The experimental-loader warning is harmless. New projects should prefer Node’s --import style or move to tsx.

Migration to tsx (the common case)#

- "dev": "ts-node-esm src/server.ts",
- "migrate": "ts-node --transpile-only ./scripts/migrate.ts",
+ "dev": "tsx watch src/server.ts",
+ "migrate": "tsx ./scripts/migrate.ts",

Verify with npm run dev after replacement. Things that need extra care:

  • --require hooks (e.g. dotenv/config) → use tsx’s --env-file (Node 20.6+) or wrap the script entry.
  • paths aliases → add tsconfig-paths and pass --require tsconfig-paths/register.
  • experimentalDecorators → still works with tsx since esbuild supports them, but emitDecoratorMetadata requires the TypeScript compiler — stay on ts-node for legacy NestJS / TypeORM.

Production deployment#

Same rule as tsx: don’t. Build to JavaScript with tsc and ship the JS:

tsc -p tsconfig.json --outDir dist
node dist/server.js

Output:

Server listening on :3000

The legacy exception is NestJS, TypeORM, and other decorator-metadata-heavy frameworks where tsc’s emitDecoratorMetadata is required and people occasionally ship ts-node-running containers in dev/staging. Even there, the right answer is “build with tsc, ship dist/”.

Performance tuning#

ts-node is the slow path. Optimisations:

Use --transpile-only#

ts-node --transpile-only ./script.ts

Output:

hello from script.ts

Skips the type-checker entirely — ~5× faster cold start. Pair with tsc --noEmit in a pre-commit hook so type errors don’t disappear.

Swap to SWC#

npm install -D @swc/core @swc/helpers

Output:

added 2 packages in 3s
// tsconfig.json
{
  "ts-node": {
    "swc": true
  }
}
ts-node ./script.ts

Output:

Hello (transpiled by SWC, ~3× faster than tsc)

--swc skips type-checking and uses SWC’s Rust transpiler. Effectively turns ts-node into a slower tsx.

Disable diagnostics in CI#

TS_NODE_LOG_ERROR=true and TS_NODE_PRETTY=false reduce stdout overhead in CI logs. TS_NODE_IGNORE_DIAGNOSTICS=2304,2307 suppresses specific error codes that are intentional (e.g. when stubbing imports).

Version migration guide#

From → ToHighlights
8 → 9Node 12 dropped; default module resolution updated to match newer TS releases.
9 → 10ESM loader stabilised; ts-node-esm binary added. --esm flag introduced. Decorator metadata behaviour tightened.
10.x ongoingBundled TS version range expanded to 4.x and 5.x; --swc and --transpile-only got first-class config. Maintenance mode — no breaking changes expected.

There has been talk of an 11.x line aligned with Node 22’s native TS stripping, but no public roadmap commits to it as of writing.

Security considerations#

  1. Arbitrary code execution. Same as any TS runner — ts-node ./untrusted.ts runs the file with full Node privileges. Sandbox untrusted code.
  2. tsconfig.json extends chain. ts-node follows extends recursively; a malicious config in a dependency could control compile output. Pin tsconfig dependencies.
  3. @types/* typosquatting. ts-node doesn’t audit @types/* packages; they’re code (declaration files), and a malicious one could declare types that mask runtime bugs. Pin via lockfile.
  4. experimentalDecorators + emitDecoratorMetadata. Decorator metadata embeds type names in compiled output — those can leak source structure to anyone with access to compiled JS or stack traces.
  5. Loader hook ordering. When chaining loaders (--loader ts-node/esm --loader @cspotcode/source-map-support), order matters. Register source-map handlers BEFORE ts-node so stack-frame rewriting sees the original locations.

Testing & CI integration#

# .github/workflows/ci.yml — legacy ts-node setup
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci
      - run: npx tsc --noEmit
      - run: npx ts-node --transpile-only ./scripts/seed.ts
      - run: npm test

Most newer projects replace npx ts-node with npx tsx in CI scripts — same surface, faster.

Jest integration uses ts-jest (its own transformer, not ts-node); Mocha integration uses --require ts-node/register in .mocharc.json. New projects should prefer vitest, which transpiles natively.

Ecosystem integrations#

  • NestJS — historically shipped with a ts-node-based start:dev script. Newer NestJS templates use SWC. Migration to tsx is straightforward.
  • TypeORM CLI — depends on ts-node when running migrations against .ts migration files. The typeorm-ts-node-commonjs adapter is the current path; works without modification.
  • Mocha--require ts-node/register in .mocharc.json. Vitest is the modern alternative.
  • Sequelize CLI / Strapi — both still ship ts-node-based dev modes; their respective issue trackers track tsx migration.
  • VS Code launch configs"runtimeArgs": ["-r", "ts-node/register"] is still the canonical “debug TypeScript” snippet.

Troubleshooting common errors#

Error [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".ts"#

ESM mode requires ts-node-esm or --loader ts-node/esm. Default ts-node only handles CJS.

TS2307: Cannot find module '@/utils'#

paths aliases need tsconfig-paths/register loaded before the script:

ts-node -r tsconfig-paths/register ./src/server.ts

Output:

Server listening on http://localhost:3000

SyntaxError: Cannot use import statement outside a module#

Same as ESM-vs-CJS confusion above. Add "type": "module" or rename to .mts, and use ts-node-esm.

Could not find a declaration file for module 'foo'#

ts-node respects strict: true. Either install @types/foo, add declare module 'foo' in a .d.ts, or pass --transpile-only to skip the check.

REPL prints “experimental warning” stack traces#

The ESM loader is experimental; the warning is benign. Suppress with NODE_OPTIONS='--no-warnings' ts-node-esm.

When NOT to use this#

  • New projects. Pick tsx. ts-node is in maintenance, not active development. Migration costs are low.
  • Simple type-stripping needs on Node 22+. Use node --experimental-strip-types — zero dependencies, native speed.
  • Production servers. Build to JS with tsc, then node dist/. Runtime transpile is wasted CPU.
  • Edge / serverless environments. Cold-start cost (loading the entire TS compiler) is enormous. Build at deploy time.
  • Decorator-free codebases. The main reason to stay on ts-node is emitDecoratorMetadata — without that, tsx is strictly better.

See also#