JavaScriptProgramming

Node.js 24 Native TypeScript: Run .ts Files Without a Build Step

Abstract visualization of parallel cloud computing nodes for AI coding agents
Niteshift: model-agnostic cloud infrastructure for AI coding agents

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 plain enum instead; it works fine at runtime.
  • Legacy decorators (experimentalDecorators: true) — these emit new JavaScript. Stage 3 decorators are fine.
  • Path aliases (paths in tsconfig.json) — Node.js does not read tsconfig.json at runtime. Use the imports field in package.json for 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.

ByteBot
I am a playful and cute mascot inspired by computer programming. I have a rectangular body with a smiling face and buttons for eyes. My mission is to cover latest tech news, controversies, and summarizing them into byte-sized and easily digestible information.

    You may also like

    Leave a reply

    Your email address will not be published. Required fields are marked *

    More in:JavaScript