What is ts-node and Why It Still Matters

ts-node Logo

ts-node lets you run TypeScript files directly in Node.js without having to compile them first. You just run ts-node myfile.ts and it works. Well, eventually - after it spends 3-5 seconds starting up because it's actually running the TypeScript compiler under the hood.

Here's the thing everyone gets wrong: ts-node isn't just a convenience tool, it's fundamentally different from the new hotness like TSX or Node.js native TypeScript support. Those tools just strip out your types and run the JavaScript. ts-node actually compiles your TypeScript with proper type checking, which means it'll catch your fuckups before they crash your program.

Why ts-node is Slow (And Why That Might Be OK)

ts-node hooks into Node.js's module loading system and runs the actual TypeScript compiler on every file it encounters. This is why it's slow - it's not just transforming your code, it's actually checking all your types, validating your imports, and making sure your decorators are set up correctly.

When you run ts-node server.ts on a decent-sized project, here's what happens:

This is why that "quick test script" takes 5 seconds to start when it should take 50ms. But here's what you get for that pain:

  • Real type checking that catches errors before runtime
  • Proper source maps that actually point to your TypeScript files
  • Full support for decorators, enums, and other TypeScript-specific features
  • A REPL that actually understands your types

The Competition That's Eating ts-node's Lunch

TSX came along and said "fuck type checking, let's just run the code fast." It uses esbuild to strip types in milliseconds instead of compiling them properly. For most development, this is totally fine - you run `tsc --noEmit` separately to check types, and tsx for actually running code.

Interestingly, one 2024 benchmark found that ts-node with nodemon is actually 3x faster than tsx on large projects. Performance varies wildly - I've seen tsx blow up on 50k line codebases while ts-node chugs along fine.

Node.js v22.6.0+ added native TypeScript support that's even more basic - it literally just removes type annotations and runs the result. No type checking, no fancy features, just raw speed. Started in v22.6.0 and improved in later versions.

The brutal truth is that for 80% of TypeScript development, you don't need ts-node's type checking at runtime. You just want to run your goddamn code without a build step.

When You Actually Need ts-node

NestJS Framework Architecture

You still want ts-node when:

  • You have decorators everywhere (especially class-validator, TypeORM, NestJS stuff)
  • You're debugging type errors and need accurate stack traces
  • You're writing complex generic code that breaks when types are stripped
  • You're too lazy to set up a separate type checking step
  • You're working on tooling/build scripts where correctness matters more than speed

I've seen teams switch to tsx and then come crawling back to ts-node when their decorator-heavy NestJS app started throwing TypeError: Cannot read properties of undefined (reading 'metadata') errors in production. Type stripping breaks more shit than people expect - especially when reflect-metadata gets confused.

The Honest Comparison: What Actually Matters

What You Care About

ts-node

TSX

Node.js Native (v22.6.0+)

Startup Time

Painfully slow (usually 3-5 seconds)

Fast (around 50ms)

Fast (like 100ms)

Memory Usage

Eats RAM (200MB+ on real projects)

Reasonable (around 60MB)

Light (maybe 30MB)

Catches Type Errors

Yes, before they crash your app

No, you need tsc --noEmit

No, just strips types

Decorators Work

Yes, properly

Mostly (some edge cases break)

No

ESM Imports

Works but config hell

Works out of the box

Works out of the box

Path Mapping

Works with tsconfig

Works with tsconfig

You're fucked, manual setup

Breaks When You Update

Rarely

Sometimes with exotic TS features

Breaks with anything complex

npm install time

Slow (pulls in TypeScript)

Fast (just esbuild)

None (built-in)

Production Use

Don't even think about it

Nope, compile first

Hell no

Installation (And All The Ways It Can Go Wrong)

The Basic Install That Should Work

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

That's it. Don't install ts-node globally unless you enjoy version conflicts and random breaks when you update Node.js.

Actually Running ts-node (The Simple Way)

## Just run a TypeScript file
npx ts-node myfile.ts

## Start a REPL that actually understands your types
npx ts-node

## Run some quick TypeScript code
npx ts-node -e \"console.log('Hello from TypeScript')\"

The ESM Import Hell

If you're using ES modules (import/export), ts-node will shit the bed with \"Cannot use import statement outside a module\" errors. Here's how to unfuck it:

Option 1: Add to your package.json

{
  \"type\": \"module\"
}

Option 2: Use the ESM loader

node --loader ts-node/esm myfile.ts

Option 3: Fix your tsconfig.json

{
  \"compilerOptions\": {
    \"module\": \"ESNext\",
    \"moduleResolution\": \"node\",
    \"allowSyntheticDefaultImports\": true,
    \"esModuleInterop\": true
  },
  \"ts-node\": {
    \"esm\": true
  }
}

You'll probably need to try all three before it works. Full ESM guide here.

Making ts-node Less Painfully Slow

ts-node is slow because it actually type-checks your code. If you just want to run your TypeScript without waiting 5 seconds:

{
  \"ts-node\": {
    \"transpileOnly\": true,
    \"files\": true
  }
}

`transpileOnly` skips type checking and makes ts-node almost as fast as tsx. But then why are you using ts-node?

Path Mapping That Actually Works

If you have path mapping in your tsconfig (@/components, etc.), ts-node needs extra help:

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

Don't forget to install tsconfig-paths:

npm install -D tsconfig-paths

Common Failures You'll Hit

\"Cannot find module\" errors: Your tsconfig probably has \"files\": false somewhere. ts-node can't find your files. Add this:

{
  \"ts-node\": {
    \"files\": true
  }
}

Memory errors on large projects: ts-node eats RAM. If Node.js crashes with heap out of memory:

node --max-old-space-size=4096 -r ts-node/register myfile.ts

Random decorator errors: Make sure you have:

{
  \"compilerOptions\": {
    \"experimentalDecorators\": true,
    \"emitDecoratorMetadata\": true
  }
}

Version conflicts: If you're getting weird TypeScript errors, check that your ts-node and TypeScript versions are compatible. ts-node v10.9.x breaks with TypeScript 5.2+ in some configurations - this GitHub issue saved my ass. The compatibility matrix exists for a reason.

Testing Setup That Doesn't Suck

For Mocha:

{
  \"scripts\": {
    \"test\": \"mocha -r ts-node/register --extensions ts 'test/**/*.ts'\"
  }
}

For Jest, don't use ts-node. Use ts-jest or you'll hate your life. Full testing guide here.

The Nuclear Option

When nothing works, delete node_modules and try again:

rm -rf node_modules package-lock.json
npm install

This fixes ts-node problems about 60% of the time - takes 2 minutes if you're lucky, 20 minutes if npm decides to be a pain in the ass. TypeScript tooling is fragile as hell.

FAQ: Real Problems, Real Solutions

Q

My ts-node script takes forever to start. What the hell?

A

ts-node is slow because it's actually compiling your TypeScript with full type checking. On a decent project, expect 3-5 seconds startup time. If it's taking longer:

  1. Check your tsconfig.json: If you have "strict": true and a million source files, it's going to crawl
  2. Use transpileOnly: Add "transpileOnly": true to your ts-node config
  3. Reduce included files: Add specific "include" paths instead of scanning everything
  4. Switch to TSX: If you don't need real-time type checking, tsx starts in 50ms
Q

"Cannot use import statement outside a module" - How do I fix this nightmare?

A

This is the #1 ts-node error that makes people quit. The module system is fucked and here's how to unfuck it:

Quick fixes to try in order:

  1. Add "type": "module" to your package.json
  2. Use node --loader ts-node/esm yourfile.ts instead of ts-node
  3. Add "module": "ESNext" and "esModuleInterop": true to tsconfig.json
  4. Change your import to use .js extensions: import './file.js' (yes, .js even though it's .ts)

If those don't work, your tsconfig is probably fucked. This Stack Overflow answer has saved my ass multiple times.

Q

My decorators aren't working. What did I miss?

A

Decorators are experimental and fragile as hell. You need all of these in your tsconfig.json:

{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "target": "ES2020"
  }
}

If you're using class-validator, TypeORM, or NestJS and decorators aren't working, this is probably why. Also make sure you have reflect-metadata imported somewhere:

import 'reflect-metadata';
Q

ts-node is eating all my RAM. How do I fix memory issues?

A

ts-node can eat 400MB+ on large projects (I've seen it hit 800MB on our microservice codebase) because it keeps the entire TypeScript compiler in memory. Solutions:

  1. Increase Node.js memory: node --max-old-space-size=4096 yourscript.js
  2. Use transpileOnly: Skips type checking, uses way less memory
  3. Split your tsconfig: Smaller include patterns = less memory usage
  4. Just switch to TSX: Uses 1/5th the memory for most use cases
Q

Should I use ts-node in production?

A

Fuck no. Never run ts-node in production. It's slow, uses tons of memory, and adds unnecessary complexity.

Compile your TypeScript to JavaScript with tsc and run the JavaScript. Anyone running ts-node in production is asking for performance issues.

Q

tsx vs ts-node - which one should I actually use?

A

Use TSX if:

  • You care about startup speed (it's 10x faster)
  • You have a simple TypeScript setup
  • You're already running tsc --noEmit for type checking
  • You don't use complex decorators

Use ts-node if:

  • You have decorators everywhere (Nest

JS, TypeORM, etc.)

  • You need real-time type checking during development
  • You're debugging complex type errors and need better error messages
  • You're too lazy to set up separate type checking

Most teams end up using tsx for development and proper compilation for production.

The Reality: ts-node's Place in 2025

What's Actually Happening with TypeScript Execution

ts-node used to be the only game in town for running TypeScript directly. Now we have tsx crushing it on speed, Node.js adding native support, and Deno/Bun offering their own takes. ts-node is no longer the default choice - it's become the tool you use when you need specific features.

Node.js native TypeScript started in v22.6.0 and keeps getting better. It just strips types and runs your code. No decorators, no fancy features, but it's fast and has zero dependencies. For simple scripts, it's perfect. Official Node.js TypeScript docs here.

TSX won the speed war. It starts 10x faster than ts-node and uses way less memory. Most development teams switched to tsx + `tsc --noEmit` for type checking. It works great until you hit edge cases.

ts-node is becoming the specialist tool. You use it when tsx breaks, when you need decorators to work properly, or when you're debugging weird type errors and need better stack traces.

Who Still Needs ts-node

Framework-heavy projects: If you're using NestJS, TypeORM, class-validator, or any decorator-heavy framework, tsx will randomly break your shit. ts-node handles decorators that actually work because it compiles them instead of just stripping types.

Complex TypeScript setups: Advanced generics, conditional types, template literal types - tsx's esbuild transpiler sometimes chokes on complex TypeScript features that ts-node handles fine.

Testing environments: A lot of testing setups still use ts-node because test runners need accurate stack traces and proper source maps. When tests fail, you want to know exactly which line of TypeScript code broke.

Build and automation scripts: DevOps scripts where correctness matters more than speed. We had a deployment script fail silently for weeks with tsx because it was ignoring type errors - cost us 3 hours of downtime when it finally broke production.

What This Means for Your Project

Most teams should try tsx first. It's faster, uses less memory, and works fine for 80% of TypeScript code. Set up `tsc --noEmit` to catch type errors and you're good.

Switch to ts-node if tsx breaks. When you hit decorators not working, weird transpilation bugs, or need better debugging, ts-node is your fallback.

Use Node.js native for simple scripts. If you're just running basic TypeScript with interfaces and simple classes, node --experimental-strip-types myfile.ts is fast as hell.

The Long-term Picture

ts-node isn't dying, it's finding its niche. As Node.js native TypeScript support improves, and tools like tsx get better at handling edge cases, ts-node's role will shrink but remain important.

Five years from now, ts-node will probably be the tool you reach for when everything else fails. Kind of like how we use webpack when Vite can't handle some weird edge case. Specialized, but essential for complex scenarios.

The TypeScript execution ecosystem is healthier with multiple tools. Speed for development (tsx), simplicity for scripts (Node.js native), and correctness for complex cases (ts-node). Pick the right tool for your specific situation.