Skip to main content

Node.js Native TypeScript Support: Toolchain 2026

·PkgPulse Team

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-types flag 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 reads tsconfig.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 resolve paths from 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 --noEmit for type checking; a bundler for production; tsconfig.json for IDE/LSP support
  • package.json type: "type": "module" required for ESM TypeScript files to run directly

At a Glance

CapabilityNode.js Nativets-node/tsxtsc + 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 version22.6+AnyAny
Setup requiredNone (22.18+/23.6+)npm installtsc setup
Cold start overheadNone~200msPre-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
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

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.