mrkeyoor.com_
Thu 06 Aug 02:08 UTC
npmCLI & Toolingupdated 05 Aug 2026

ts-node

ts-node runs TypeScript files on Node without a build step. It hooks Node's module loading, so when something requires or imports a .ts file, ts-node hands the source to the real TypeScript compiler, gets JavaScript back, and lets Node execute it, keeping source maps so stack traces point at your .ts lines. It reads your tsconfig.json, adds its own options under a "ts-node" key, and can either typecheck as it goes (the default, and the thing that sets it apart from every esbuild-based runner) or skip checking with transpileOnly for speed. It also gives you a TypeScript REPL and register hooks that other tools such as Mocha can load.

Verdict

Still the only common runner that typechecks while it executes, and it works fine on the CommonJS projects that already depend on it. For anything new, use tsx or Node's own type stripping and run tsc --noEmit separately; ts-node has not shipped a release since December 2023 and its ESM path is built on an API node has moved away from.

API stability4/5The CLI, register hook, and tsconfig options have not changed since 10.x in 2021, so nothing breaks under you; that stability is partly because development stopped, and the unreleased 11.0.0 beta is where the changes are parked.
Docs5/5typestrong.org/ts-node documents every flag, both module systems, per-tool recipes for Mocha, AVA, Gulp and VS Code, and a troubleshooting section that names the actual error strings people search for.
Maintenance1/5Last publish December 2023, last repository push July 2024, an 11.0.0 beta from October 2023 never promoted, and 198 open issues plus 34 open PRs with no triage.
Ecosystem4/5Around 48M downloads a week and a register hook that Mocha, Gulp, TypeORM, and countless config files still document, but new projects and toolchains have moved to tsx and native type stripping.

Use it if

  • You want type errors to actually stop the process at runtime, not just in your editor; every fast alternative strips types and will happily run code that does not typecheck
  • An existing CommonJS project already runs Mocha, Gulp, or a config file through node -r ts-node/register and it works, so churning to another runner buys nothing
  • You want a TypeScript REPL, or need to embed one, which createRepl() in the API supports directly
  • You need a custom compiler or TypeScript transformers in the execution path, which the compiler and transpiler options are built for
Skip it if

Setup reality

You install two packages, not one: ts-node plus typescript, since typescript is a peer dependency resolved from your project first and a global install falls back to the global compiler. Depending on your config you may also need @types/node and tslib. Configuration lives in three places at once and they interact: node flags must be passed to node or through NODE_OPTIONS because the ts-node binary rejects them, ts-node CLI flags must come before the entrypoint, and most options are better written into the "ts-node" block of tsconfig.json so they also apply when a tool loads ts-node/register. The two errors everyone hits are ERR_REQUIRE_ESM, from mixing an ESM dependency into CommonJS output, and ERR_UNKNOWN_FILE_EXTENSION, from running an extensionless binary such as mocha under the ESM loader. When configuration goes sideways, ts-node --showConfig and ts-node -vv tell you what it actually loaded, which is faster than guessing.

Patterns

Execute a TypeScript filerun-a-script

npm install -D ts-node typescript @types/node

npx ts-node src/script.ts

# faster: skip typechecking
npx ts-node --transpileOnly src/script.ts
npx ts-node-transpile-only src/script.ts

ts-node flags must come before the file. Anything after it is passed to your script, so ts-node script.ts --transpileOnly silently does nothing you wanted.

Put ts-node options in tsconfig.jsonconfigure-via-tsconfig

{
  "extends": "ts-node/node16/tsconfig.json",
  "ts-node": {
    "transpileOnly": true,
    "files": true,
    "compilerOptions": { "module": "CommonJS" }
  },
  "compilerOptions": {
    "module": "ESNext",
    "target": "es2022",
    "strict": true
  }
}

The nested ts-node.compilerOptions override the outer ones for ts-node only, which is how you keep ESM output for your bundler while running CommonJS locally. This block is the only configuration that applies when another tool loads ts-node/register.

Load ts-node inside another node processregister-hook

node -r ts-node/register src/index.ts

# reach child processes and tools too
NODE_OPTIONS="-r ts-node/register --no-warnings" node ./index.ts

# skip typechecking via the dedicated entrypoint
node -r ts-node/register/transpile-only ./index.ts

node flags cannot be given to the ts-node binary at all; either invoke node directly like this or set NODE_OPTIONS. NODE_OPTIONS also propagates into child processes and worker threads, which is usually what you want and occasionally not.

Run as native ESMrun-native-esm

// package.json: { "type": "module" }
// tsconfig.json: { "compilerOptions": { "module": "ESNext" }, "ts-node": { "esm": true } }

npx ts-node-esm src/index.ts
node --loader ts-node/esm src/index.ts

The docs call this experimental and advise against production use. --esm spawns a child process to install the loader, and node has deprecated --loader in favor of --import with module.register, so expect warnings and version-specific breakage.

Swap the transpiler for SWCuse-swc-transpiler

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

// tsconfig.json
{
  "ts-node": { "swc": true }
}

swc: true implies transpileOnly, so you lose typechecking entirely in exchange for a large speedup. SWC uses @swc/helpers rather than tslib, so install it if importHelpers is on.

Make tsconfig paths work at runtimeresolve-path-aliases

npm i -D tsconfig-paths

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

ts-node deliberately does not implement paths, because tsc does not rewrite import specifiers either. Without this, an import of @app/foo compiles fine and then throws MODULE_NOT_FOUND at runtime.

Run Mocha tests written in TypeScriptrun-mocha-tests

// .mocharc.json (CommonJS)
{
  "require": "ts-node/register",
  "extensions": ["ts", "tsx"],
  "spec": "tests/**/*.spec.ts"
}

// .mocharc.json (native ESM)
{
  "loader": "ts-node/esm",
  "extensions": ["ts", "tsx"]
}

Mocha's binary has no file extension, which native ESM rejects with ERR_UNKNOWN_FILE_EXTENSION; ts-node 10.6 and newer include a workaround, so do not pin below that if you are on the ESM path.

Run one config file as CommonJS inside an ESM projectoverride-module-type-per-file

{
  "ts-node": {
    "transpileOnly": true,
    "moduleTypes": {
      "webpack.config.ts": "cjs",
      "scripts/**/*": "cjs"
    }
  },
  "compilerOptions": { "module": "es2020" }
}

This exists because a webpack.config.ts cannot be given its own package.json to flip "type". Overridden files are compiled with isolatedModules limitations, and moduleTypes can only be set in tsconfig.json or through the API, never as a CLI flag.

Start a TypeScript REPL or evaluate an expressiontypescript-repl

npx ts-node
> const x: number = 41
> x + 1
42

npx ts-node -e 'const s: string = "hi"; console.log(s)'
npx ts-node -p -e '"Hello, world!"'
echo 'console.log(1 as number)' | npx ts-node

Top-level await works in the REPL. With no entrypoint the tsconfig search happens relative to the current directory rather than a script, so a REPL started from a subdirectory may pick up a different config than you expect.

Write a standalone executable TypeScript scriptshebang-script

#!/usr/bin/env ts-node
// options come from tsconfig.json, not the shebang

console.log('hello from a .ts script');

Passing flags in a shebang needs env -S, which is missing on older systems, so put options in tsconfig.json for portability. chmod +x the file or the shebang does nothing.

Register ts-node from codeprogrammatic-register

// bootstrap.js
require('ts-node').register({
  transpileOnly: true,
  project: './tsconfig.build.json',
  compilerOptions: { module: 'CommonJS' },
});

require('./src/index.ts');

register() installs the hook for every later require, so it must run before you import any .ts file. create() builds the same compiler service without installing hooks, which is what you want if you are embedding rather than executing.

Find out what config is actually in usedebug-configuration

npx ts-node --showConfig
npx ts-node -vv
# ts-node v10.9.2
# node v22.14.0
# compiler v5.6.3

TS_NODE_DEBUG=true npx ts-node src/index.ts

--showConfig prints the merged compilerOptions plus the resolved "ts-node" block, including which tsconfig.json won. This is the fastest way to explain why one directory typechecks and another does not.

Alternatives

PackageRegistryPick it when
tsxnpmYou want the same run-a-.ts-file experience, much faster, actively released, and with working ESM and CommonJS support, and you do not need typechecking at runtime.
@swc-node/registernpmYou want a Rust-based register hook for existing -r workflows such as Mocha, with optional typechecking in a separate process.
esbuild-registernpmYou want a minimal esbuild-powered require hook for scripts and config files and nothing else.