
Node.js 24 is the current LTS, and it ships with native TypeScript support enabled by default. That means node app.ts just works — no tsconfig, no tsc, no ts-node, no esbuild. Node runs your TypeScript directly. The feature is called type stripping, and it hit stable in v24.12.0. If you’re still compiling to a dist/ folder for your internal tools, cron jobs, or CLI scripts, you’re probably adding complexity you don’t need.
How Type Stripping Actually Works
The mechanism is simpler than it sounds. Node.js uses Amaro — a thin Node.js wrapper around SWC’s WebAssembly TypeScript parser — to strip type annotations before V8 ever sees your code. When Node.js encounters a .ts, .mts, or .cts file, Amaro removes type annotations, interfaces, type aliases, and any other erasable TypeScript syntax, then hands plain JavaScript to V8.
One design choice worth noting: stripped syntax is replaced with whitespace, not deleted. This keeps line numbers identical, so you can debug TypeScript source directly without source maps. It’s a clean approach.
What Node.js is not doing is type checking. It is a type stripper, not a type checker. That distinction matters a lot — more on that shortly.
What Works and What Breaks
Most TypeScript code works fine with native stripping. Anything that is purely an annotation — type aliases, interfaces, generic type parameters, import type, union and intersection types — gets stripped cleanly. Stage 3 decorators (the modern standard) also work.
The failure cases are TypeScript features that generate JavaScript rather than being removed from it:
const enum— inlines values at compile time. Use a plainenuminstead; it works fine at runtime.- Legacy decorators (
experimentalDecorators: true) — these emit new JavaScript. Stage 3 decorators are fine. - Path aliases (
pathsin tsconfig.json) — Node.js does not readtsconfig.jsonat runtime. Use theimportsfield inpackage.jsonfor subpath mapping instead. - Parameter properties (
constructor(private foo: string)) — these generate constructor assignment code that can’t just be stripped. - Namespaces with runtime code — same issue as above.
The rule of thumb: if a TypeScript feature emits new JavaScript code rather than disappearing at runtime, native stripping won’t handle it. Stick to erasable syntax and you’re fine.
One Trip Wire: File Extensions in Imports
Node.js does not guess extensions for TypeScript files. You have to be explicit:
// Correct
import { parseConfig } from './config.ts'
// Will likely fail or silently resolve to .js
import { parseConfig } from './config'
This catches developers coming from ts-node or bundlers that handle extension resolution automatically. Node’s module resolver is stricter — use the full .ts extension in local imports.
When to Use It (And When Not To)
Native type stripping is not a universal replacement for a build pipeline. Cold starts take a hit — every import is parsed and stripped at startup, and that overhead scales with module count. For a production service importing hundreds of packages, pre-compiled JavaScript still starts faster.
Where native stripping shines:
- CLI tools and developer scripts
- Background workers and cron jobs
- Serverless functions (Lambda, Cloudflare Workers, etc.)
- Internal tooling and one-off automation
- Simple APIs with a modest dependency tree
Where you still want tsc or a bundler like tsup:
- Production services importing many packages
- NestJS or decorator-heavy applications using legacy decorators
- Anything using tsconfig path aliases you don’t want to migrate
- When cold-start latency is a hard requirement
The honest take: if your project is a NestJS monolith using experimentalDecorators and a dozen paths aliases, don’t migrate today. But if you’re maintaining TypeScript scripts, worker jobs, or a small internal API? You’re running a build step for no reason.
Keep tsc — Just Don’t Build With It
Dropping the build step doesn’t mean dropping type safety. You still need tsc --noEmit in CI. Your package.json scripts look like this:
{
"scripts": {
"typecheck": "tsc --noEmit",
"start": "node src/index.ts",
"dev": "node --watch src/index.ts"
}
}
Run typecheck in CI. Ship with confidence. The tsc step stays; the dist/ folder disappears.
Docker Before and After
The Dockerfile change is the most visible win. Here’s a before and after for a typical worker service:
# Before — two-stage build
FROM node:24-alpine AS builder
WORKDIR /app
COPY package*.json .
RUN npm ci
COPY . .
RUN npm run build
FROM node:24-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
CMD ["node", "dist/index.js"]
# After — no build step
FROM node:24-alpine
WORKDIR /app
COPY package*.json .
RUN npm ci
COPY . .
CMD ["node", "src/index.ts"]
Fewer layers, faster image builds, less surface area. For services where startup time isn’t a hard constraint, this is a clear win.
The Verdict
Native TypeScript in Node.js 24 is production-usable for the right workloads. It’s not “full TypeScript support” — it’s type stripping, and knowing the difference prevents nasty surprises. But for the large category of Node.js code that isn’t a high-traffic production server — your CLI, your cron job, your Lambda, your build script — the build step was always overhead. Node.js 24 just makes that official.
The official Node.js TypeScript documentation covers the stable API surface, and the Node.js “Running TypeScript Natively” guide walks through the initial setup. For a detailed comparison of Node.js native stripping against Bun and Deno, jsmanifest’s type-stripping comparison is worth the read.













