Node.js Native TypeScript Execution Complete Guide
Baseline version: Node.js 24 LTS. Behavioral changes in Node.js 26 (such as changes to enum handling) are covered in a separate section within the document.
node app.ts just works — and at some point that became true. No build step, no extra tooling, no flags. With type stripping promoted to Stable in Node.js 24 LTS, running .ts files directly has become the official standard workflow.
This article covers three things: how type stripping works internally, concrete patterns for deploying to production without a tsc build step, and a decision framework for distinguishing when to use native execution from when not to. The third is the most directly useful in practice.
One prerequisite: This entire article assumes an ESM (ECMAScript Modules) codebase. "module": "NodeNext", the imports field in package.json, and the .ts extension convention all operate in an ESM environment. If you are using a CommonJS codebase, some settings apply differently; those points are marked separately in the body.
When to Use Native Execution and When Not To
Before introducing the concepts, here is the decision framework. Checking whether your project meets these conditions is the fastest starting point.
Core Concepts
What Is Type Stripping
TypeScript type annotations play no role at runtime. So they can simply be removed before execution. That is type stripping. Unlike tsc, which transforms or downlevels code, type stripping replaces type-related syntax with whitespace of equal length.
// Original .ts
function greet(name: string): string {
return `Hello, ${name}`;
}
// After stripping (actual code passed to V8)
function greet(name ) {
return `Hello, ${name}`;
}Because the replacement uses whitespace, line numbers are preserved. This is why source maps are unnecessary. The line numbers in stack traces match the original .ts file exactly.
The component responsible for this is Amaro — the built-in TypeScript loader for Node.js, which internally wraps @swc/wasm-typescript, a WebAssembly build of the Rust-based SWC parser. It is bundled with Node.js and requires no separate installation.
Amaro is not an external tool; it is a built-in implementation at the Node.js loader layer.
Evolution by Version
As Deno and Bun gained attention for their native TypeScript support, the Node.js ecosystem moved quickly to follow. The table below is based on each release's notes.
| Version | Change |
|---|---|
| Node.js v22.6 (2024-08) | Initial introduction of the --experimental-strip-types flag |
| Node.js v22.7 (2024-08) | Added --experimental-transform-types (enum and namespace support) |
| Node.js v23.6 (2025-01) | Enabled by default without a flag |
| Node.js v22.18 / v24.3 | Experimental warning messages removed (per each release note) |
| Node.js v24.12 | Type stripping officially promoted to Stable (per release notes) |
| Amaro v1.0 (2025-06) | Official loader 1.0 release, TypeScript 5.8 support, node_modules processing enabled (per release notes) |
Node.js 26 Changes (Separate Section): The
--experimental-transform-typesflag was removed in Node.js 26. It is still available in Node.js 24 LTS, but designs that depend on this flag will require significant rework during a future version upgrade. Enum handling is moving toward build-tool layers rather than the runtime.
Supported and Unsupported Syntax
Type stripping does not handle all TypeScript. It only handles erasable syntax. Syntax that generates runtime code cannot simply be erased.
Supported (erasable syntax):
- Type annotations, interfaces, type aliases
import type, genericsascasts,satisfiesoperator
Unsupported (runtime code generation):
enum— generates actual JavaScript objectsconst enum— generates inlined constants- Parameter properties (
constructor(private name: string)) - Namespaces containing runtime code
- Legacy
experimentalDecorators(used in older versions of NestJS, etc.)
"Can't I just use the
--experimental-transform-typesflag?" In Node.js 24 LTS, enabling this flag allowsenumand namespaces to be transformed at runtime. However, since this flag was removed in Node.js 26, any new project design that relies on it will require a complete overhaul during a future version upgrade. The recommended production strategy is to replaceenumwith literal unions oras constobjects.
One more important point: Type stripping does not perform type checking. Code with type errors will run at runtime regardless. This is why you must always run tsc --noEmit separately in CI.
Practical Application
Development Environment — Watch Mode
This is the simplest starting point. All you need is a tsconfig.json.
node --watch src/index.tsNo nodemon, no ts-node, no tsx. The process restarts automatically when files change. Add it directly to package.json scripts.
{
"scripts": {
"dev": "node --watch src/index.ts",
"start": "node src/index.ts",
"typecheck": "tsc --noEmit"
}
}The .cts and .mts extensions are supported in the same way. Native execution also applies in mixed-module environments where CommonJS files are explicitly named .cts.
tsconfig.json Configuration
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"skipLibCheck": true,
"erasableSyntaxOnly": true
}
}The "erasableSyntaxOnly": true option causes tsc --noEmit to flag syntax that type stripping cannot handle (such as enum) as an error. If you have decided to switch to native execution, this is the first thing you should enable.
Path Alias Handling
The paths configuration in tsconfig.json — path aliases like @/utils — is not resolved at runtime. Node.js does not look at tsconfig.json to find modules.
The solution is the imports field in package.json.
{
"imports": {
"#utils/*": "./src/utils/*.ts",
"#config": "./src/config/index.ts"
}
}// Usage
import { logger } from '#utils/logger.ts';In a native execution environment, use the .ts extension as-is. Adding "allowImportingTsExtensions": true to tsconfig.json allows the tsc --noEmit step to handle these extensions without errors. Using # instead of @ as a prefix is a requirement of the Node.js imports field.
If your existing codebase makes heavy use of @/ path aliases, consider the migration cost. In that case, keeping tsx or your existing build step may be the more practical choice.
Docker Production Image Optimization
The difference becomes clear when comparing against a traditional multi-stage build.
Traditional approach:
# Build stage
FROM node:24-slim AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY src/ ./src/
COPY tsconfig.json .
RUN npm run build
# Runtime stage
FROM node:24-slim
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY --from=builder /app/dist ./dist
CMD ["node", "dist/index.js"]Native execution:
FROM node:24-slim
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY src/ ./src/
# tsconfig.json is not read at runtime, so COPY can be omitted
CMD ["node", "src/index.ts"]The typescript package, ts-node, build scripts, the dist/ directory, and the multi-stage build all disappear. Both image size and build time are reduced.
Why
--omit=dev: In npm v9+, the--productionflag is deprecated;--omit=devis the currently recommended approach.
Deploying .ts Handlers Directly to AWS Lambda
With AWS Lambda officially supporting the Node.js 24 runtime, it is now possible to package .ts handlers directly in container-image-based Lambda functions.
// handler.ts
import type { APIGatewayProxyEvent } from 'aws-lambda';
export const handler = async (event: APIGatewayProxyEvent) => {
return {
statusCode: 200,
body: JSON.stringify({ message: 'Hello from TypeScript' }),
};
};Packaging source directly without a dist/ compilation step simplifies the deployment stage.
However, cold start performance requires attention. Native execution runs Amaro and SWC-WASM on every init to perform type stripping. A pre-compiled .js artifact avoids that cost. A smaller package size does not directly translate to improved cold start times; the actual impact depends on function size and the execution environment. If cold start performance is a critical requirement, measure in your actual environment before deciding.
CI Pipeline Configuration
# .github/workflows/ci.yml
jobs:
ci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '24'
- name: Install dependencies
run: npm ci
- name: Type check
run: npx tsc --noEmit
- name: Test
run: node --test
- name: Smoke test (verify app startup)
run: node src/index.tsType checking and execution are separated. tsc --noEmit only checks types and does not emit files, making it suitable for verifying type safety without a build. The final step is a smoke test to confirm the app starts successfully. Replace it with your integration test suite command if you have one.
Pros and Cons Analysis
Advantages
| Item | Details |
|---|---|
| Eliminates build pipeline | No tsc compilation step — shorter CI times, fewer Docker layers |
| Faster startup time | In local development, roughly 9× faster startup than ts-node (sebastian-staffa.eu; on a 600-line Express server, ts-node ~1.4s vs. native under 200ms; may vary by version and environment) |
| No source maps needed | Types are replaced with whitespace, so line numbers match the original .ts |
| Zero configuration | Works in Node.js 24+ without additional flags |
| Simplified supply chain | Can remove devDependencies such as ts-node and tsx |
| Built-in watch mode | No need for nodemon when combined with node --watch |
Disadvantages and Limitations
| Item | Details |
|---|---|
| No type checking | Does not catch type errors at execution time. Must run tsc --noEmit separately in CI |
| No enum support (default mode) | const enum and regular enum are unavailable in default mode. Can be worked around with --experimental-transform-types, but that flag was removed in Node.js 26 |
| No tsconfig path alias support | paths mappings are not resolved at runtime. Must replace with the imports field in package.json |
| No legacy decorator support | Decorators based on experimentalDecorators: true are not supported (older versions of NestJS, etc.) |
| Lambda cold start | SWC-WASM startup cost incurs on every init. May be neutral to worse compared with pre-compiled JS |
Common Mistakes in Practice
1. Skipping type checking
Because node app.ts works fine, type checking can get dropped from CI. Type stripping does not validate types. You need to explicitly add tsc --noEmit to your CI pipeline.
2. Hitting a runtime error from enum usage
Migrating an existing codebase that contains enum will produce a SyntaxError. Adding "erasableSyntaxOnly": true as mentioned in the tsconfig configuration section lets you catch incompatible syntax in the tsc --noEmit step beforehand. This is why enabling it first is recommended when switching to native execution.
3. Expecting path aliases to be resolved at runtime
@/utils/logger is in tsconfig so it works fine in the IDE, but running the app throws Cannot find module. Node.js does not read paths from tsconfig.json. Switch to the imports field in package.json, or consider continuing to use tsx.
4. Attempting this on Node.js below v22.6
This feature does not exist below v22.6. The flag itself will not be recognized. Check your infrastructure's Node.js version first.
Closing Thoughts
Type stripping is a mechanism that replaces TypeScript type annotations with whitespace so V8 can execute the code directly. It was promoted to Stable in Node.js 24 LTS and is available without any additional configuration. The practical benefits are the elimination of the build step and lighter Docker images; since type checking is not performed, tsc --noEmit must be kept in CI. For projects that use enum, legacy decorators, or complex path aliases, tsx or an existing build step remains the more stable choice.
If you want to start applying this right now, follow these steps.
Step 1 — Check whether your current project uses enum or legacy decorators.
Add "erasableSyntaxOnly": true to tsconfig.json and run tsc --noEmit to identify all incompatible syntax at once.
Step 2 — Try it in your development environment first.
Change the dev script in package.json from ts-node or tsx to node --watch src/index.ts. You will see file-change detection working without a separate build.
Step 3 — Explicitly add tsc --noEmit to your CI pipeline.
Even after switching to native execution, CI is responsible for maintaining type safety. Pairing it with node --test in the pipeline gives you a complete TypeScript workflow with no build step.
References
- Running TypeScript Natively — Node.js Official Guide
- Modules: TypeScript — Node.js v26.5.0 Documentation
- Node.js Moves Toward Stable TypeScript Support With Amaro 1.0 — InfoQ
- Node.js 24 Native TypeScript: Running .ts Files in Production Without a Build Step — jsmanifest
- Node.js Native TypeScript in 2026: Type Stripping Guide — HireNodeJS
- Running TypeScript in Node.js: tsx vs. ts-node vs. native — LogRocket Blog
- TSX vs Native Node.js TypeScript — Better Stack
- A look at native TypeScript performance — sebastian-staffa.eu
- Node.js v23 Natively Supports TypeScript — Medium
- Node.js 26 Quietly Killed Your TypeScript Enums — jsgurujobs
- GitHub — nodejs/amaro: Node.js TypeScript wrapper
- Node.js stabilizes built-in TypeScript execution — Progosling News
- AWS Lambda: Node.js 24 runtime now available
- TypeScript Without tsc in 2026: Type-Stripping in Node.js 24, Bun, and Deno Compared — jsmanifest