Run TypeScript Directly in Node.js: Type Stripping Explained

Node.js can execute .ts files natively. Run node app.ts, and the runtime removes the type annotations, replaces them with whitespace, and executes the remaining JavaScript. No ts-node, no tsx, no build step. This guide covers how type stripping works, which syntax survives it, how to configure tsconfig.json, and how to migrate an existing project.

Quick Takeaways

  • Type stripping is stable in current Node.js. Node.js v24.12.0 (LTS) marked it stable, and the v25 documentation lists it at Stability 2.
  • Node.js replaces TypeScript syntax with whitespace and performs no type checking. Run tsc --noEmit separately.
  • Only erasable syntax works. enum, namespace with runtime code, and constructor parameter properties need --experimental-transform-types or a rewrite.
  • Use import type for type-only imports and write the .ts extension in relative import paths.
Approach Build Step Type Checking Startup Overhead Non-erasable Syntax
Node.js type stripping None No (use tsc) Very low Unsupported by default
tsx / ts-node None (loader) ts-node optional Moderate Supported
tsc → JavaScript Yes Yes None at runtime Supported
Bundler (esbuild, swc) Yes No None at runtime Supported

What Is Type Stripping in Node.js?

Type stripping deletes TypeScript-only syntax from a file and leaves valid JavaScript behind. It does not compile or transform your code. It blanks out type annotations, interfaces, and type aliases.

Consider this input:

const total: number = 5;

Node.js turns it into:

const total         = 5;

The annotation becomes spaces. This keeps every token at its original line and column. Stack traces point to the right place without source maps, and Node.js does not generate any.

Node.js uses Amaro, a wrapper around the SWC TypeScript parser compiled to WebAssembly, to do the stripping. You never import it directly.

Which Node.js Version Do You Need?

Node.js Version Status
22.6 Introduced behind --experimental-strip-types
22.18 / 23.6 Enabled by default, with an experimental warning
24.12 (LTS) Marked stable, per the release notes
25.x Documented as Stability 2: Stable

Use Node.js 24 LTS or newer for production work. Run node --version to check.

Syntax Breakdown: Erasable vs. Non-Erasable

The rule is simple. If deleting a piece of syntax leaves valid JavaScript with identical behavior, it is erasable. If TypeScript must generate new JavaScript to replace it, it is not.

Erasable Syntax (Works Out of the Box)

// types.ts
export interface User {
  id: number;
  name: string;
}

export type Role = "admin" | "editor" | "viewer";
// greet.ts
import type { User, Role } from "./types.ts";

// Parameter and return annotations are erased.
export function greet(user: User, role: Role = "viewer"): string {
  return `Hello ${user.name} (${role})`;
}

// Generics and type assertions are erased too.
const items = new Map<number, User>();
const el = document.body as HTMLElement;

Erasable features include:

  • Type annotations, interfaces, and type aliases
  • Generics and as assertions
  • The satisfies operator
  • declare statements and abstract modifiers
  • Access modifiers (private, readonly) on class fields

Non-Erasable Syntax (Throws an Error)

These features require TypeScript to emit extra JavaScript, so type stripping rejects them:

// enum.ts - FAILS under default type stripping
enum Status {
  Active,
  Inactive,
}
console.log(Status.Active);
node enum.ts
# SyntaxError / ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX

The error fires because an enum compiles to an object with reverse mappings. There is no whitespace substitution that produces that object.

The same restriction applies to:

  • Enums (regular and const enum)
  • Namespaces and modules that contain runtime code
  • Constructor parameter properties (constructor(private x: number))
  • Legacy decorators and import aliases such as import x = require()

Node.js also does not provide polyfills and will not support decorators until the language standardizes them.

To allow these features anyway, pass the transform flag:

node --experimental-transform-types app.ts

Treat that flag as a stopgap. Rewriting the code is the durable fix.

Practical Implementation

Step 1: Write and Run a TypeScript File

// server.ts
import { createServer } from "node:http";
import type { IncomingMessage, ServerResponse } from "node:http";

interface HealthPayload {
  status: "ok";
  uptime: number;
}

function handler(_req: IncomingMessage, res: ServerResponse): void {
  const body: HealthPayload = { status: "ok", uptime: process.uptime() };
  res.writeHead(200, { "Content-Type": "application/json" });
  res.end(JSON.stringify(body));
}

createServer(handler).listen(3000, () => {
  console.log("Listening on http://localhost:3000");
});
node server.ts

Output:

Listening on http://localhost:3000

The server starts immediately. Node.js strips the import type line and every annotation before V8 sees the code.

Step 2: Configure tsconfig.json for Type Checking

Node.js ignores tsconfig.json at runtime. Your editor and tsc still read it, so configure it to flag syntax that Node.js cannot strip. The Node.js docs recommend TypeScript 5.8 or newer. Use these settings:

{
  "compilerOptions": {
    "target": "esnext",
    "module": "nodenext",
    "moduleResolution": "nodenext",
    "allowImportingTsExtensions": true,
    "rewriteRelativeImportExtensions": true,
    "erasableSyntaxOnly": true,
    "verbatimModuleSyntax": true,
    "noEmit": true
  }
}

What each option does:

Option Purpose
erasableSyntaxOnly Makes tsc error on enums, namespaces, and parameter properties
verbatimModuleSyntax Forces import type for type-only imports
allowImportingTsExtensions Permits import "./x.ts"
rewriteRelativeImportExtensions Rewrites .ts to .js if you later emit with tsc
noEmit Uses tsc only as a checker

The erasableSyntaxOnly flag is the most useful. It turns a runtime crash into an editor error.

Step 3: Add Scripts to package.json

{
  "type": "module",
  "scripts": {
    "start": "node src/server.ts",
    "dev": "node --watch src/server.ts",
    "typecheck": "tsc --noEmit",
    "test": "node --test"
  }
}
  • node --watch restarts on file changes with no extra tooling.
  • node --test discovers and runs .ts test files.
  • npm run typecheck is your only type-checking step. Run it in CI.

Edge Cases and Gotchas

Type-Only Imports Need the type Keyword

Without the type keyword, Node.js treats the import as a value import, which causes a runtime error. The stripper works file by file. It cannot know whether User is a type or a value in another module.

// BAD: Node.js keeps this import and looks for a runtime export named User
import { User } from "./types.ts";

// GOOD: Node.js removes the whole statement
import type { User } from "./types.ts";

// GOOD: Inline form keeps the value import and strips the type specifier
import { createUser, type User } from "./user.ts";

File Extensions Are Mandatory

Node.js does not guess extensions. Write the real one.

import { greet } from "./greet.ts";   // Works
import { greet } from "./greet";      // ERR_MODULE_NOT_FOUND

Files in node_modules Are Rejected

To discourage publishing raw TypeScript packages, Node.js refuses to handle TypeScript files inside folders under a node_modules path. Publish compiled JavaScript with .d.ts files for libraries. Type stripping is intended for applications, scripts, and tooling.

No Type Checking at Runtime

This code runs without complaint:

const count: number = "not a number";
console.log(count);

Output:

not a number

Node.js erased the annotation and never compared types. Only tsc --noEmit catches this.

Run TypeScript from --eval and STDIN

Type stripping also works with --eval and STDIN.

node --input-type=module-typescript --eval "const n: number = 42; console.log(n)"

Use this for quick one-liners.

Refactoring: Bad Code vs. Good Code

Non-erasable syntax is the main migration cost. These rewrites remove it.

Replace Enums with Const Objects

Bad (anti-pattern under type stripping):

enum Direction {
  Up,
  Down,
  Left,
  Right,
}

function move(dir: Direction): void {
  console.log(Direction[dir]);
}

Good (erasable and tree-shakeable):

const Direction = {
  Up: "UP",
  Down: "DOWN",
  Left: "LEFT",
  Right: "RIGHT",
} as const;

// Derive the union type from the object values.
type Direction = (typeof Direction)[keyof typeof Direction];

function move(dir: Direction): void {
  console.log(dir);
}

move(Direction.Up);

Output:

UP

The as const assertion and the type alias are erased. The object stays as plain JavaScript.

Replace Parameter Properties with Explicit Fields

Bad:

class Account {
  constructor(
    private readonly id: string,
    public balance: number,
  ) {}
}

Good:

class Account {
  readonly #id: string;
  balance: number;

  constructor(id: string, balance: number) {
    this.#id = id;
    this.balance = balance;
  }
}

The #id private field is native JavaScript, so it also gives you true runtime privacy.

Replace Namespaces with ES Modules

Bad:

namespace Validators {
  export function isEmail(value: string): boolean {
    return value.includes("@");
  }
}

Good:

// validators.ts
export function isEmail(value: string): boolean {
  return value.includes("@");
}
import * as Validators from "./validators.ts";
Validators.isEmail("dev@example.com");

Type Stripping vs. tsx vs. tsc

Criteria Type Stripping tsx tsc + node
Install required None Dev dependency Dev dependency
Supports enum No (flag needed) Yes Yes
Reads tsconfig paths No Yes Yes (with extra tooling)
Type safety Manual tsc --noEmit Manual Built in
Best for Scripts, servers, CI tasks Legacy codebases Published libraries
Learning curve Low Low Medium

Choose type stripping for new applications. Choose tsx when you depend on paths aliases or non-erasable features and cannot refactor yet. Choose tsc when you publish a package.

Migration Checklist

  1. Upgrade to Node.js 24 LTS or newer.
  2. Upgrade TypeScript to 5.8+ and set erasableSyntaxOnly: true.
  3. Run tsc --noEmit and fix every reported enum, namespace, and parameter property.
  4. Add type to every type-only import (verbatimModuleSyntax enforces this).
  5. Add .ts extensions to all relative imports.
  6. Replace paths aliases with the package.json imports field (#utils/*).
  7. Swap ts-node or tsx in scripts for plain node.
  8. Keep tsc --noEmit in CI as the type gate.

Here is the imports field replacement for path aliases:

{
  "imports": {
    "#utils/*": "./src/utils/*.ts"
  }
}
import { slugify } from "#utils/slugify";

Strategic FAQ

Does Node.js type check TypeScript files?

No. Node.js removes types and runs the remaining JavaScript. It does not verify them. Run tsc --noEmit in your editor and CI to catch type errors.

Which Node.js version supports running TypeScript natively?

Node.js 22.6 added it behind --experimental-strip-types. It became default behavior in later 22.x and 23.x releases. Node.js v24.12.0 (LTS) marked type stripping as stable. Use Node.js 24 LTS or newer.

Why does Node.js throw an error on my TypeScript enum?

An enum generates runtime JavaScript, and type stripping only erases syntax. Node.js errors on features that replace TypeScript syntax with new JavaScript unless --experimental-transform-types is passed. Replace the enum with an as const object, or enable the flag.

Can I use type stripping for npm packages?

No. Node.js refuses .ts files under node_modules. Compile to JavaScript and ship .d.ts declarations when you publish.

Hot this week

Robotics Predictions for 2027

Seven data-driven robotics predictions for 2027: VLA policies, humanoid actuators, ROS 2 real-time, safety filters, and code. Read the engineering roadmap.

Robots and Jobs: What the Evidence Says in 2026

Robots and jobs in 2026: IFR installation data, wage and employment estimates, and a payback model engineers can run. Read the evidence.

Robotics Careers in 2026: Roles, Skills, and Salaries

Explore robotics careers in 2026: top roles, must-have ROS 2 and controls skills, and U.S. salary ranges. Build your robotics career roadmap today.

Robot Sensors Explained: How Robots Perceive the World

Learn how LiDAR, IMU, encoders, and depth cameras work, with fusion math and ROS2 code. Build a reliable perception stack today.

20 Beginner Robot Projects Ranked by Difficulty

Build 20 beginner robot projects ranked from easiest to hardest, with wiring, code, and ROS2 steps. Pick your first build and start today.

Topics

Robotics Predictions for 2027

Seven data-driven robotics predictions for 2027: VLA policies, humanoid actuators, ROS 2 real-time, safety filters, and code. Read the engineering roadmap.

Robots and Jobs: What the Evidence Says in 2026

Robots and jobs in 2026: IFR installation data, wage and employment estimates, and a payback model engineers can run. Read the evidence.

Robotics Careers in 2026: Roles, Skills, and Salaries

Explore robotics careers in 2026: top roles, must-have ROS 2 and controls skills, and U.S. salary ranges. Build your robotics career roadmap today.

Robot Sensors Explained: How Robots Perceive the World

Learn how LiDAR, IMU, encoders, and depth cameras work, with fusion math and ROS2 code. Build a reliable perception stack today.

20 Beginner Robot Projects Ranked by Difficulty

Build 20 beginner robot projects ranked from easiest to hardest, with wiring, code, and ROS2 steps. Pick your first build and start today.

Robot Computers in 2026: Raspberry Pi 5, Jetson, and Industrial PCs Compared

Raspberry Pi 5 vs NVIDIA Jetson vs industrial PCs for ROS 2 robots. See specs, latency math and a decision matrix, then pick your compute.

AI in Robotics: Programmed Behavior vs Machine Learning (and When You Need Each)

Programmed control or machine learning? Compare PID, FSMs, and RL with ROS2 code, benchmarks, and a decision framework. Pick the right one.

What Is ROS 2? A Beginner’s Guide to the Robot Operating System in 2026

Learn what ROS 2 is, how nodes, topics, and DDS work, and build your first publisher and subscriber. Start your ROS 2 project today.

Related Articles

Popular Categories