Right, let’s talk about running your TypeScript code. Because tsc is great and all, but constantly compiling to a dist directory and then running it feels like building a ship in a bottle. You just want to run the darn thing. You have three main choices for this, and each has its own particular brand of genius and madness.

The Old Guard: ts-node

ts-node is the veteran. It’s been around the block, it’s seen things, and it just works. Under the hood, it’s a clever hack. It registers a hook with Node.js that says, “Hey, anytime someone tries to require a .ts or .tsx file, call me first.” It then grabs the TypeScript source, compiles it on the fly in memory, and hands the compiled JavaScript back to Node to execute.

The simplest way to use it is to just install it (npm i -D ts-node) and then run:

npx ts-node my-script.ts

For a more robust project, especially one with import paths that need resolving (like @/modules/my-stuff), you’ll want to use ts-node-dev for a better development experience. It’s essentially ts-node wrapped with node-dev (a fancier nodemon), so it restarts your process on file changes. Your package.json script might look like this:

{
  "scripts": {
    "dev": "ts-node-dev --respawn --transpile-only src/server.ts"
  }
}

Notice the --transpile-only flag. This is the secret sauce. It tells the TypeScript compiler to skip the type checking phase and just strip the types. Why? Because type checking is slow. You should be doing that in your IDE and your linting step anyway. Let ts-node handle the transpilation, which is blazingly fast by comparison. Forgetting this flag is a common pitfall that leads to developers staring at a terminal, wondering why their “hello world” script is taking five seconds to start.

The New Hotness: tsx

If ts-node is the reliable veteran, tsx is the new special forces operative. It’s built on top of the native Node.js ESM loader, which makes it significantly faster and more lightweight. It doesn’t mess around with the old require hook system; it uses the modern, official ESM hooks.

Using it is dead simple:

npx tsx my-script.ts

Its performance is excellent, and it “just gets” ESM. The best part? You can also use it as a replacement for node itself to run your compiled JavaScript, making it a fantastic universal runner. You can even set it as your script runner in your package.json:

{
  "type": "module",
  "scripts": {
    "dev": "tsx watch src/server.ts"
  }
}

The main pitfall here is assuming everyone is on a new-enough version of Node to support its modern underpinnings. But honestly, if you’re not, you have bigger problems.

Going Fully Native: Node.js ESM Loader

Now, for the purists. Since Node.js v20.11 (a.k.a. the “they finally figured it out” release), you can run .ts files directly with the native node command. No third-party tools required. This is achieved by using the --loader flag. Well, technically it’s --import now, but the internet hasn’t quite caught up yet.

The command looks a bit arcane, but it’s powerful:

node --import tsx/esm my-script.ts

Wait, hold on. Did I just use tsx to run something “natively”? Yes, yes I did. The current native solution is essentially a collaboration. You’re telling Node, “Use the ESM loaders from the tsx package to handle .ts files.” This is the official, blessed approach. The goal is for this to eventually be a built-in flag like --loader ts, but we’re not quite there yet.

The huge advantage of this method is that it’s the closest you can get to the metal. It’s the future. It integrates perfectly with other native Node.js features and debugging tools. The downside? It’s still a bit behind a flag and requires you to remember that slightly janky command. It’s not quite “batteries-included” perfection, but it’s getting there fast.

So, Which One Should You Use?

Here’s my direct advice, straight from the trenches:

  • For a new project today? Use tsx. It’s fast, simple, and aligns with the future of Node.js (ESM). The developer experience is top-notch.
  • Stuck on an older, massive CommonJS project? ts-node-dev with --transpile-only is your safe, battle-tested bet. It won’t surprise you.
  • Feeling adventurous and want to be on the cutting edge? Script your commands to use the native node --import tsx/esm approach. You’ll be ready when it becomes the default.

No matter which you choose, always, always run type checking separately. Use tsc --noEmit or vue-tsc --noEmit or whatever your setup requires in your CI/CD pipeline and your pre-commit hooks. Let your runner handle fast transpilation, and let the type checker do its one job perfectly. Trust me, your sanity will thank you.