Node.js Native TypeScript Support: Saying Goodbye to ts-node and tsx
Prologue: The Obvious Yet Non-Obvious Execution of TS
When I first learned web development, one of the most paradigm-shifting tools I encountered was TypeScript. Perhaps because of my background in history—where verifying vague historical sources and checking cross-references was second nature—adding TypeScript's explicit safety checks to JavaScript's flexible and sometimes fragile type system felt incredibly reassuring.
However, as I started configuring actual projects, a massive wave of configuration fatigue hit me. It stemmed from a very fundamental question: "Why can't I just run a TypeScript file (.ts) directly inside Node.js?"
My transition through various build pipelines and execution workarounds looked something like this:
- Translating TypeScript via
tscinto adist/folder, then runningnode dist/index.js(extremely tedious because I had to rebuild on every change). - Installing
ts-node(which usually led to days of debugging ES Modules vs. CommonJS compatibility issues betweentsconfig.jsonandpackage.json). - Recently, relying on the modern
tsxpackage to handle the development server runtime.
I always wished running a server file could be as simple as running a Python script. When Node.js introduced the --experimental-strip-types option in version 22.6.0, that thirst for simplicity was finally quenched.
Concept: What is Type Stripping?
When I first heard that Node.js could execute TypeScript directly, my immediate thought was, "Has the heavy TypeScript compiler (tsc) been bundled inside the Node engine?" The actual implementation, however, is much lighter and far more elegant: Type Stripping.
Type Stripping is the process of "cutting out the type annotations from the TypeScript code (stripping them away) and executing the remaining, pure JavaScript code directly."
The key takeaway here is: Node.js does not perform any Type Checking.
// Original TypeScript code
function greet(name: string): string {
return `Hello, ${name}!`;
}
// Result after Type Stripping (Pure JavaScript)
function greet(name) {
return `Hello, ${name}!`;
}
The TypeScript compiler is heavy because it has to analyze types, infer implicit definitions, and check for compile-time errors. Node.js skips all of these computationally heavy tasks entirely. It simply sweeps the codebase at the parser level and erases type declarations.
Once I understood this, I had an epiphany: "Of course! Browsers and runtimes don't care whether the types are correct at runtime. That validation is only needed during development inside the IDE (like VS Code) or within CI/CD pipelines!"
By decoupling execution from validation, Node.js achieves direct TypeScript support without sacrificing startup speed.
Deep Dive: Under the Hood of --experimental-strip-types and Its Limits
Node's native TypeScript support leverages a lightweight parser library written in Rust called amaro (which is essentially a subset of SWC). Once amaro erases the type annotations, the stripped JavaScript is fed directly into Node's internal V8 engine.
Because it only performs a text-level stripping rather than full transpilation (like Webpack or Esbuild would), there are a few strict limitations you must keep in mind:
1. Inability to Use TS-only Syntaxes That Emit Runtime Code
Certain TypeScript features don't just disappear as types—they actually generate runtime JavaScript objects. Node's native strip feature cannot handle these automatically because they cannot be processed by simply erasing text.
- Standard Enums: Enums are compiled into actual JavaScript objects. Trying to run code containing standard enums will fail in Node. You should use
const enumor standard JavaScript objects (as const) instead. - Parameter Properties in Classes: Shortcut declarations like
constructor(public name: string)(which automatically declare and assign class fields) do not work. You must declare properties and assign them manually. - Namespaces: The TypeScript-specific
namespacesyntax is banned because it outputs actual runtime closure objects.
2. The Absence of Compile-time Validation
As mentioned, Node.js only erases the type annotations. This means that even if your code has severe type mismatches, it will execute regardless.
// This code will run without syntax errors in Node.js
let message: string = "Hello";
message = 123; // This should throw a type error, but Node strips the type and runs it.
Consequently, you must still run tsc --noEmit in your local development watch mode or inside your CI/CD test runner to catch type errors before deploy.
Application: Boot Time Comparison and Refactoring Docker Builds
I wanted to test this in a small side project to evaluate performance gains and see how it affected my configuration files.
I measured the cold start speed of a standard dev server using the popular tsx package versus Node's native stripping option.
# 1. Running with tsx
$ time npx tsx src/server.ts
Server running on port 3000...
real 0m0.485s
# 2. Running with Node.js native stripping
$ time node --experimental-strip-types src/server.ts
Server running on port 3000...
real 0m0.180s
While tsx is already incredibly fast thanks to esbuild, native execution with Node's built-in C++ and Rust parser cut the cold boot time by more than half (approx. 2.5x faster). The instant restarts during development felt noticeably smoother.
Even better was the simplification of my Docker configurations. Previously, I had to compile files into a dist/ directory during a multi-stage build, or include development dependencies like ts-node in the final production image.
# Legacy Dockerfile strategy (Multi-stage compilation)
FROM node:22 AS builder
WORKDIR /app
COPY . .
RUN npm install
RUN npm run build # Outputs JavaScript in dist/
FROM node:22-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package.json ./
RUN npm install --only=production
CMD ["node", "dist/server.js"]
Using native TypeScript stripping, we can skip the compile step altogether:
# Modern simplified Dockerfile
FROM node:22-alpine
WORKDIR /app
COPY . .
RUN npm install --only=production
# Directly run the TypeScript file using native stripping
CMD ["node", "--experimental-strip-types", "src/server.ts"]
This drastically reduced build times and solved common head-scratchers like missing source maps or out-of-sync dist/ folders during production debugging.
Summary: The Freedom of Native Runtimes
Studying history taught me that technology tends to follow a recurring pattern. Early innovations are often chaotic and rely on a constellation of fragmented, third-party tooling. As the technology matures, the most critical features are absorbed into the core platform (the runtime).
The JavaScript ecosystem, with its complex web of bundlers, transpilers, and config files, is no exception. Now that TypeScript is the industry standard, it was only a matter of time before runtimes began accepting it natively. While runtimes like Deno and Bun pioneered this, Node.js has finally caught up.
Although the --experimental-strip-types flag is still technically experimental and has syntax limitations, experiencing the pure simplicity of running "just node" directly on a TypeScript file makes me confident that the backend development environment is heading towards a much cleaner future.