Node.js Native TypeScript Support: Toolchain 2026
Node.js can run TypeScript files directly in 2026. No ts-node, no tsx, no tsc compile step — just node file.ts. This has been the "I can't believe we need a tool for this" problem in the JavaScript ecosystem for a decade. It's now solved, with an important asterisk: Node.js strips types, it doesn't transform them. Understanding that distinction is the key to knowing what changes in your toolchain and what doesn't.
TL;DR
Node.js 22.18+ and 23.6+ run TypeScript files by default — just node file.ts, no flag, no ts-node. The underlying engine is Amaro, which strips type annotations without ever reading tsconfig.json or checking types. This works for most TypeScript code but doesn't handle syntax that requires code generation: enum, const enum, namespace, parameter properties (constructor(private x: T)), experimentalDecorators, path alias resolution, or JSX. For those, use --experimental-transform-types or keep a build step. For development scripts and tools, you can now delete ts-node (which has been in maintenance mode since 2022). For type checking, you still need tsc --noEmit. For production, you still need a bundler.
Key Takeaways
- Node.js 22.6+:
--experimental-strip-typesflag required - Node.js 22.18+ / 23.6+: on by default — just run
node file.ts, no flag needed - Powered by Amaro (wraps
@swc/wasm-typescript) — not the TypeScript compiler; never readstsconfig.json - What it does: strips type annotations (
: string,interface Foo {},type Bar = ...) using whitespace replacement - What it doesn't do: transform
enum,const enum,namespace,experimentalDecorators, parameter properties, or resolvepathsfrom tsconfig --experimental-transform-types: opt-in flag that enables enums, namespaces, and parameter properties (re-enters experimental)- You can remove:
ts-node(maintenance mode since 2022) for most development workflows - You still need:
tsc --noEmitfor type checking; a bundler for production;tsconfig.jsonfor IDE/LSP support package.jsontype:"type": "module"required for ESM TypeScript files to run directly
At a Glance
| Capability | Node.js Native | ts-node/tsx | tsc + node |
|---|---|---|---|
Run .ts files | ✅ | ✅ | ✅ (compiled) |
| Type checking | ❌ | ❌ | ✅ (with --noEmit) |
enum (legacy) | ❌ | ✅ | ✅ |
namespace | ❌ | ✅ | ✅ |
paths alias | ❌ | ✅ (with plugin) | ✅ (with tsc-paths) |
experimentalDecorators | ❌ | ✅ | ✅ |
| JSX/TSX | ❌ | ✅ | ✅ (with config) |
| Source maps | ✅ (v22.7+) | ✅ | ✅ |
| Node.js version | 22.6+ | Any | Any |
| Setup required | None (22.18+/23.6+) | npm install | tsc setup |
| Cold start overhead | None | ~200ms | Pre-compiled |
How It Works
Node.js uses Amaro (which wraps @swc/wasm-typescript) to strip type annotations — not the TypeScript compiler. Amaro is a fast, deliberate type eraser: it replaces type syntax with whitespace (not deletion), which keeps column numbers correct for stack traces without needing a full source map. Type stripping is purely additive: TypeScript-valid syntax that doesn't require transformation runs as-is. Node.js never reads tsconfig.json — it runs your TypeScript regardless of what's configured there.
// This TypeScript code runs directly with node --strip-types
interface User {
id: number
name: string
}
async function getUser(id: number): Promise<User> {
const response = await fetch(`/api/users/${id}`)
return response.json() as User
}
// After type stripping (what Node.js actually runs):
async function getUser(id) {
const response = await fetch(`/api/users/${id}`)
return response.json()
}
Enabling Native TypeScript Support
Node.js 22.6 - 23.5 (experimental flag):
node --experimental-strip-types server.ts
Node.js 23.6+ / 22.18+ (on by default — no flag needed):
# Just works — type stripping enabled by default, no flag required
node server.ts
# --watch for dev server behavior
node --watch src/index.ts
Node.js 22.6 - 22.17 (experimental flag):
node --experimental-strip-types server.ts
Via package.json scripts (22.18+ / 23.6+):
{
"scripts": {
"start": "node src/index.ts",
"dev": "node --watch src/index.ts"
}
}
With --experimental-transform-types (enables enums, namespaces, parameter properties):
# Opt back into experimental if your codebase uses transformed TypeScript features
node --experimental-transform-types server.ts
What Works: The Common Cases
TypeScript Interfaces and Type Aliases
// ✅ Works — interfaces are fully erased
interface ApiResponse<T> {
data: T
status: number
message: string
}
type UserId = string | number
type Handler = (req: Request, res: Response) => void
Generic Functions
// ✅ Works — generics are erased
function first<T>(arr: T[]): T | undefined {
return arr[0]
}
async function fetchJson<T>(url: string): Promise<T> {
const res = await fetch(url)
return res.json()
}
Type Assertions and Satisfies
// ✅ Works
const config = {
port: 3000,
host: 'localhost',
} satisfies ServerConfig
const user = data as User
const el = document.getElementById('app') as HTMLDivElement
Typed Parameters and Return Types
// ✅ Works — all annotations stripped
function createServer(config: ServerConfig): Server {
return new Server(config)
}
class UserService {
private readonly db: Database
constructor(db: Database) {
this.db = db
}
async findById(id: string): Promise<User | null> {
return this.db.users.findOne({ id })
}
}
What Doesn't Work: TypeScript Transforms
These features require a compilation step because they produce JavaScript output that doesn't exist in the source:
Enums (Legacy)
// ❌ Fails — regular enum compiles to a JS object (code generation required)
enum Direction {
Up,
Down,
Left,
Right,
}
// ❌ Also fails — const enum requires tsc to inline values at compile time
// (Node.js has no compile step, so inlining never happens)
const enum Direction {
Up = 'UP',
Down = 'DOWN',
}
// ✅ Use a plain const object — the modern, erasable alternative
const Direction = {
Up: 'UP',
Down: 'DOWN',
Left: 'LEFT',
Right: 'RIGHT',
} as const
type Direction = typeof Direction[keyof typeof Direction]
// ✅ OR use --experimental-transform-types if you must keep existing enums
// node --experimental-transform-types server.ts
Namespaces
// ❌ Fails — namespace compiles to an IIFE
namespace Validation {
export interface StringValidator {
isAcceptable(s: string): boolean
}
}
// ✅ Use modules instead
export interface StringValidator {
isAcceptable(s: string): boolean
}
Parameter Properties
// ❌ Fails — parameter properties emit field assignments (code generation)
class UserService {
constructor(private readonly db: Database) {}
// TypeScript generates: this.db = db ← Node.js can't do this
}
// ✅ Explicit property declaration instead
class UserService {
private readonly db: Database
constructor(db: Database) {
this.db = db
}
}
Legacy Decorators (experimentalDecorators)
// ❌ Fails with native strip-types (decorator syntax requires transformation)
@Injectable()
class UserService {
@Inject(DATABASE_TOKEN)
private db: Database
}
// ✅ Use --experimental-transform-types for decorator-heavy code
// ✅ Or use a build step (tsup, swc) for production decorator support
Path Aliases
// ❌ Path aliases in tsconfig.json DON'T resolve with --strip-types
// tsconfig.json: { "paths": { "@utils/*": ["./src/utils/*"] } }
import { formatDate } from '@utils/date' // ❌ Module not found at runtime
// ✅ Use actual relative paths (recommended for simplicity)
import { formatDate } from './src/utils/date'
// ✅ Or use import maps (Node.js native, no tsconfig needed)
// package.json:
// { "imports": { "#utils/*": "./src/utils/*.js" } }
import { formatDate } from '#utils/date' // ✅ Works with --strip-types
Updating Your Toolchain
What You Can Remove
For most projects, you can now delete these packages:
# ts-node: effectively unmaintained (last meaningful release 2022, maintenance mode)
# On Node.js 22.18+ / 23.6+, it's fully redundant for development use
npm uninstall ts-node
# Keep @types/node — still needed for TypeScript Language Server / IDE support
# tsx: redundant for basic use but still useful if you have enums, path aliases,
# or JSX — and has better error messages and faster startup than native
# Before (old scripts):
# "start": "ts-node src/index.ts"
# "migrate": "ts-node scripts/migrate.ts"
# After (Node.js 22.18+ / 23.6+):
# "start": "node src/index.ts" ← no flag needed
# "migrate": "node scripts/migrate.ts"
What You Keep
# Type checking — still essential
tsc --noEmit # Checks types without emitting files — keep in CI
# Production bundler — still needed for web apps
vite build # or tsup, esbuild, etc.
# Node.js --strip-types doesn't optimize/bundle for production
# tsup for Node.js server production builds
tsup src/index.ts # Fast ESM/CJS production build
The Recommended 2026 TypeScript Project Structure
Modern Node.js TypeScript project (scripts/tools/APIs) on Node.js 22.18+ / 23.6+:
development:
node --watch src/index.ts ← direct execution, no flag, no ts-node
type checking (CI/pre-commit):
tsc --noEmit ← still essential (Node.js never checks types)
production (Node.js server):
tsup src/index.ts --format esm,cjs ← optimized build
production (web app):
vite build ← still the right tool, handles JSX + optimizations
Mental model shift:
Node.js = the development runner
tsc = the linter/type-checker
Build tools = production artifacts only
tsconfig Changes
With native TypeScript support, your tsconfig becomes "type checking only" — it doesn't control how Node.js runs your code:
// tsconfig.json — 2026 recommended for Node.js projects
{
"compilerOptions": {
"target": "ESNext", // Type checking target
"module": "NodeNext", // Correct resolution for Node.js ESM
"moduleResolution": "NodeNext",
"strict": true,
"noEmit": true, // We're not using tsc to emit files
"skipLibCheck": true,
// Remove: outDir, declaration, declarationMap (no longer needed)
// Keep paths aliases ONLY if using import maps or a build step
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
Source Maps and Debugging
Node.js 22.7+ supports --enable-source-maps alongside --strip-types:
node --strip-types --enable-source-maps src/index.ts
With source maps enabled, error stack traces point to TypeScript line numbers, not the stripped JavaScript positions. Debuggers (VS Code, WebStorm) can set breakpoints in .ts files and hit them correctly.
The Bigger Picture: What This Means
Node.js native TypeScript support doesn't replace your entire toolchain — but it removes a real friction point for scripts, CLIs, tooling, and simple APIs. The impact is most felt in:
- Development scripts and automation — no more ts-node config, just
node file.ts - CLI tools — ship as TypeScript source, users run with Node.js directly
- Monorepo tooling — internal scripts that only run in a development context
- Simple APIs and microservices — remove the ts compilation step from the dev loop
For web apps, production Node.js servers that need optimization, or anything using JSX, you still want a bundler (Vite, tsup, esbuild). Node.js native TypeScript support solves the "running a TypeScript file" problem, not the "deploying a production application" problem.
Compare ts-node, tsx, and tsup npm downloads on PkgPulse.
Related: ts-blank-space vs Node strip-types vs swc · Bun vs Node.js 2026 · ECMAScript 2026 Features
See the live comparison
View nodejs typescript on PkgPulse →