loke.dev
Header image for The Build Step Is Now Optional

The Build Step Is Now Optional

You can finally run TypeScript directly in Node.js without a single configuration file or external dependency.

· 4 min read

Have you ever felt that sinking feeling when you just want to run a five-line script, but you end up spending twenty minutes configuring a tsconfig.json and installing a dozen devDependencies just to get it to work?

For years, the "TypeScript Tax" was a mandatory payment. If you wanted the safety of types, you had to accept the complexity of a build step. You needed tsc, or ts-node, or tsx, or some convoluted Webpack/Vite/Esbuild pipeline just to see "Hello World" in your terminal. But as of Node.js v22.6.0 (and further refined in v23), that ceremony is officially over. Node can now execute TypeScript files directly.

The "Just Run It" Workflow

Here is a perfectly valid TypeScript file. Let’s call it server.ts:

import { createServer, IncomingMessage, ServerResponse } from 'node:http';

const port: number = 3000;

const server = createServer((req: IncomingMessage, res: ServerResponse) => {
  res.writeHead(200, { 'Content-Type': 'text/plain' });
  res.end('No build step required!\n');
});

server.listen(port, () => {
  console.log(`Server running at http://localhost:${port}/`);
});

Previously, running node server.ts would result in a syntax error because Node would hit that : number or the IncomingMessage type and lose its mind. Now, you can just do this:

node --experimental-strip-types server.ts

That’s it. No node_modules required. No package.json required. No tsconfig.json required. It just... works.

How is this happening?

Node isn't actually "compiling" TypeScript in the way you might think. It's using a process called type stripping.

Under the hood, Node looks at your code, finds the TypeScript-specific syntax (type annotations, interfaces, type aliases), and effectively replaces them with whitespace. It leaves the valid JavaScript behind and executes that. It's using a lightning-fast library to do this, so the overhead is almost imperceptible.

The philosophy here is simple: if the code is valid JavaScript once the types are gone, Node should be able to run it.

The Catch (And why it’s actually a good thing)

Before you delete your dist folders and burn your build scripts, there are two major "gotchas" you need to understand.

1. No Type Checking

The flag is called --experimental-strip-types, not --experimental-type-check. Node is being a bit reckless here—it doesn't care if you try to assign a string to a variable labeled as a number.

let version: number = "v23"; // Node will run this without complaining!

If you want to catch errors, you still need your IDE (VS Code) or to run tsc --noEmit in your CI/CD pipeline. This is purely about execution, not validation.

2. No "Transformer" Features

TypeScript has a few features that aren't just "types"—they actually generate JavaScript code. These are increasingly viewed as "legacy" or "mistakes" by the modern TS community because they don't follow the "type-only" rule.

Node's type stripper will fail if you use:
- Enums
- Namespaces
- Legacy Decorators (the old version)
- Parameter Properties (e.g., constructor(public name: string) {})

If you stay within the realm of modern, clean TypeScript, you’ll be fine. If you rely heavily on Enums, you’re going to have a bad time. Honestly? This is a great excuse to stop using Enums anyway. Use literal union types instead: type Status = 'open' | 'closed'.

Why this changes the game for me

I find this most useful for "glue code." Think about those little migration scripts, cron jobs, or CLI tools that live in your repository.

Previously, I had two bad choices:
1. Write them in plain JavaScript and lose the autocomplete/safety I love.
2. Write them in TypeScript and deal with a node_modules folder the size of a small moon just to run a script that moves files around.

Now, I get the best of both worlds. I can write a script with full IDE support, use the node: prefix for built-in modules, and run it instantly.

Wait, what about Node 23?

If you’re living on the edge with Node.js 23, things get even better. They’ve introduced --experimental-transform-types. While "stripping" just removes types, "transforming" adds a bit more support for things like those pesky Enums.

However, for most of us, the goal is to keep our code as close to "JavaScript with hints" as possible.

Moving Forward

Is the build step *really* dead? For large-scale production applications, probably not yet. You’ll still want a build step to minify your code, bundle dependencies, and run a rigorous type-check.

But for development? For testing? For scripts? The barrier to entry just hit the floor. We are moving toward a future where TypeScript is a first-class citizen of the web ecosystem, and the friction of "compiling" is becoming a relic of the past.

Go ahead, try it. Run node --experimental-strip-types on that one script you’ve been meaning to fix. It feels surprisingly liberating.