Back

How to Run TypeScript Natively in Node.js

How to Run TypeScript Natively in Node.js

Node.js 23.6.0 marks a turning point for TypeScript developers: you can now execute .ts files directly without transpilation tools like ts-node or tsc. This native TypeScript support streamlines development workflows by eliminating build steps while maintaining type safety through your IDE.

This guide covers everything you need to run TypeScript natively in Node.js, from basic setup to production considerations. You’ll learn how type stripping works, which TypeScript features are supported, and how to configure your projects for optimal compatibility.

Key Takeaways

  • Node.js 23.6.0 executes TypeScript files directly without transpilation
  • Type stripping removes annotations while preserving code structure
  • Most common TypeScript features work, but enums and namespaces require workarounds
  • Import statements must include the .ts extension
  • Native execution offers 2-3x faster startup times compared to traditional tools

Quick Start: Running TypeScript in Node.js 23.6.0

Let’s start with a simple TypeScript file to demonstrate native execution:

// greeting.ts
function greet(name: string): string {
  return `Hello, ${name}!`
}

console.log(greet("TypeScript"))

With Node.js 23.6.0 or later, run this directly:

node greeting.ts

That’s it—no compilation step required. Node.js strips the type annotations and executes the remaining JavaScript.

To check your Node.js version:

node --version  # Should be v23.6.0 or higher

You’ll see an experimental warning on first run:

ExperimentalWarning: Type Stripping is an experimental feature and might change at any time

To suppress this warning in development:

node --disable-warning=ExperimentalWarning greeting.ts

Or set it permanently:

export NODE_OPTIONS="--disable-warning=ExperimentalWarning"

Understanding Type Stripping in Node.js

Type stripping differs fundamentally from traditional TypeScript transpilation. Instead of transforming TypeScript into JavaScript, Node.js simply removes type annotations while preserving the original code structure.

Here’s what happens during type stripping:

// Original TypeScript
function calculate(a: number, b: number): number {
  return a + b
}

// After type stripping (what Node.js executes)
function calculate(a        , b        )         {
  return a + b
}

Notice the whitespace preservation—this maintains accurate line numbers for debugging without requiring source maps.

Performance Benefits

Native TypeScript execution offers significant performance improvements:

  • No transpilation overhead: Direct execution without intermediate steps
  • Faster startup times: ~45ms vs. 120ms with ts-node
  • Reduced memory usage: No transpiler loaded in memory
  • Simplified debugging: Original line numbers preserved

Evolution of TypeScript Support in Node.js

Understanding the progression helps you choose the right approach:

v22.6.0: Initial Type Stripping

node --experimental-strip-types app.ts

Basic type removal only—no TypeScript-specific syntax support.

v22.7.0: Type Transformation

node --experimental-transform-types app.ts

Added support for enums and namespaces through transformation.

v23.6.0: Default Type Stripping

node app.ts  # Type stripping enabled by default

No flag needed for basic TypeScript execution.

Supported TypeScript Features and Limitations

What Works

Native TypeScript in Node.js supports most everyday TypeScript features:

// ✅ Type annotations
let count: number = 0

// ✅ Interfaces
interface User {
  id: number
  name: string
}

// ✅ Type aliases
type Status = 'active' | 'inactive'

// ✅ Generics
function identity<T>(value: T): T {
  return value
}

// ✅ Type imports
import type { Config } from './types.ts'

What Doesn’t Work

Some TypeScript-specific features require workarounds:

FeatureSupportedWorkaround
EnumsUse union types or const objects
NamespacesUse ES modules
Parameter propertiesExplicit property declarations
JSX/TSXUse traditional transpilation
DecoratorsWait for V8 support

Example workarounds:

// ❌ Enum (not supported)
enum Status {
  Active,
  Inactive
}

// ✅ Union type alternative
type Status = 'active' | 'inactive'

// ✅ Const object alternative
const Status = {
  Active: 'active',
  Inactive: 'inactive'
} as const

File Extensions and Module Systems

Node.js uses file extensions to determine module handling:

  • .ts - Follows package.json "type" field (ESM or CommonJS)
  • .mts - Always treated as ESM
  • .cts - Always treated as CommonJS

Important: Local imports must include the .ts extension:

// ❌ Traditional TypeScript
import { utils } from './utils'

// ✅ Node.js native TypeScript
import { utils } from './utils.ts'

Configuring tsconfig.json for Native TypeScript

While Node.js doesn’t read tsconfig.json during execution, proper configuration ensures IDE support and type checking:

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

Key settings explained:

  • allowImportingTsExtensions: Permits .ts in import paths
  • rewriteRelativeImportExtensions: Set to false to preserve .ts extensions
  • verbatimModuleSyntax: Enforces explicit type imports
  • noEmit: Prevents accidental JavaScript generation

Run type checking separately:

tsc --noEmit

Migration Guide from Traditional TypeScript Tools

Migrating from ts-node

  1. Remove ts-node dependency:
npm uninstall ts-node
  1. Update package.json scripts:
// Before
"scripts": {
  "dev": "ts-node src/index.ts"
}

// After
"scripts": {
  "dev": "node src/index.ts"
}
  1. Handle unsupported features (convert enums to unions)

Migrating from tsc Compilation

  1. Remove build steps:
// Remove these scripts
"scripts": {
  "build": "tsc",
  "start": "node dist/index.js"
}

// Use this instead
"scripts": {
  "start": "node src/index.ts"
}
  1. Update CI/CD pipelines to skip compilation

Performance Comparison

Native TypeScript execution shows significant improvements:

MetricNative TypeScriptts-nodetsc + node
Startup time~45ms~120ms~200ms
Memory usageBaseline+30MB+10MB
Build stepNoneNoneRequired

Common Pitfalls and Solutions

Import Path Errors

// Error: Cannot find module './utils'
import { helper } from './utils'

// Solution: Include .ts extension
import { helper } from './utils.ts'

Unsupported Syntax

// Error: Enums are not supported
enum Color { Red, Blue }

// Solution: Use const object
const Color = { Red: 0, Blue: 1 } as const

Type Checking Confusion

Remember: Node.js doesn’t perform type checking. Always run tsc --noEmit for type validation.

Production Considerations

When to Use Native TypeScript

  • Development environments
  • Prototypes and experiments
  • Small scripts and utilities
  • Teams ready for experimental features

When to Avoid

  • Production applications (until stable)
  • Projects requiring full TypeScript features
  • Teams needing stable tooling
  • Complex build pipelines

Docker Adjustments

# Before
FROM node:23-alpine
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
CMD ["node", "dist/index.js"]

# After
FROM node:23-alpine
COPY package*.json ./
RUN npm ci
COPY . .
CMD ["node", "src/index.ts"]

Conclusion

Native TypeScript support in Node.js 23.6.0 represents a significant simplification for development workflows. By eliminating transpilation steps, you can focus on writing code rather than configuring build tools. While limitations exist—particularly around TypeScript-specific syntax—the benefits for most development scenarios are compelling.

Start by trying native TypeScript in your development environment, gradually migrate existing projects, and prepare for a future where TypeScript runs everywhere JavaScript does. As this feature stabilizes, expect broader adoption and expanded capabilities in upcoming Node.js releases.

FAQs

While technically possible, it's not recommended for production use yet. The feature is still experimental and may change. Use it for development environments, prototypes, and non-critical applications. For production, continue using traditional transpilation until the feature becomes stable.

Yes, you should keep TypeScript as a development dependency for type checking. Node.js only strips types without validating them. Run tsc --noEmit separately to catch type errors during development or in your CI pipeline.

Native TypeScript requires explicit .ts extensions in import paths, unlike traditional TypeScript tooling. Update all local imports from './module' to './module.ts'. External package imports don't need extensions.

Replace enums with union types for simple cases or const objects with as const assertion for numeric enums. For string enums, union types work perfectly. This approach is actually more tree-shakeable and aligns better with modern TypeScript practices.

The Node.js team focuses on type stripping rather than full transpilation for performance reasons. Features requiring runtime transformation like enums and decorators may get support when V8 implements them natively, but the goal is fast, minimal processing rather than complete TypeScript compatibility.

Complete picture for complete understanding

Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.

Check our GitHub repo and join the thousands of developers in our community.

OpenReplay