Native TypeScript in Node.js: The --experimental-strip-types Revolution
Hello HaWkers, 2025 marked the beginning of a new era for TypeScript developers: running TypeScript code directly in Node.js, without prior transpilation.
Have you ever wondered why we need to compile TypeScript to JavaScript before running it? What if Node.js could just... ignore the types and run it directly?
The Big Change in Node.js 22+
With Node.js 22, a revolutionary experimental feature was introduced: --experimental-strip-types. This flag allows Node.js to run TypeScript files natively, removing type annotations at runtime.
The Traditional Problem
Historically, working with TypeScript in Node.js always involved multiple steps:
# Traditional flow (still common in 2024)
# 1. Write TypeScript
# 2. Compile with tsc
npx tsc
# 3. Run the generated JavaScript
node dist/index.js
# Or use tools like ts-node
npx ts-node src/index.tsTools like ts-node, tsx, and ts-node-dev emerged to simplify, but still depended on in-memory transpilation.
How --experimental-strip-types Works
Node.js 22+ introduced a radically different approach: instead of transpiling TypeScript to JavaScript, it simply removes type annotations and runs the code.
Practical Example
// user.ts - Pure TypeScript code
interface User {
id: number;
name: string;
email: string;
createdAt: Date;
}
class UserService {
private users: Map<number, User> = new Map();
createUser(name: string, email: string): User {
const id = this.users.size + 1;
const user: User = {
id,
name,
email,
createdAt: new Date()
};
this.users.set(id, user);
return user;
}
getUser(id: number): User | undefined {
return this.users.get(id);
}
getAllUsers(): User[] {
return Array.from(this.users.values());
}
}
// Usage
const service = new UserService();
const user = service.createUser('Jeff', 'jeff@example.com');
console.log(user);Before (Node.js 20 and earlier):
# Needed to compile first
npx tsc user.ts
node user.js
# Or use ts-node
npx ts-node user.tsNow (Node.js 22+):
# Direct execution!
node --experimental-strip-types user.tsWhat's Happening Under the Hood
Node.js is not compiling TypeScript. It's doing something simpler and faster:
// Original TypeScript
function greet(name: string): string {
return `Hello, ${name}!`;
}
const user: User = { name: 'Jeff', age: 30 };
// What Node.js actually executes (types removed)
function greet(name) {
return `Hello, ${name}!`;
}
const user = { name: 'Jeff', age: 30 };
Modern Setup for TypeScript + Node.js in 2025
Let's create a modern project taking advantage of these new capabilities:
1. Project Initialization
# Create directory
mkdir modern-node-ts
cd modern-node-ts
# Initialize package.json
npm init -y
# Install TypeScript (only for type checking)
npm install -D typescript @types/node
# Create tsconfig.json
npx tsc --init2. tsconfig.json Configuration
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"lib": ["ES2022"],
"moduleResolution": "bundler",
"esModuleInterop": true,
"strict": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
// For type checking only, doesn't generate output
"noEmit": true
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}3. Package.json with Modern Scripts
{
"name": "modern-node-ts",
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "node --experimental-strip-types --watch src/index.ts",
"start": "node --experimental-strip-types src/index.ts",
"type-check": "tsc --noEmit",
"check": "tsc --noEmit && echo '✓ Type check passed'"
},
"devDependencies": {
"@types/node": "^22.0.0",
"typescript": "^5.6.0"
}
}4. Complete Application Example
// src/config.ts
export interface AppConfig {
port: number;
host: string;
env: 'development' | 'production' | 'test';
}
export const config: AppConfig = {
port: parseInt(process.env.PORT || '3000'),
host: process.env.HOST || 'localhost',
env: (process.env.NODE_ENV as AppConfig['env']) || 'development'
};
// src/database.ts
export interface DatabaseRecord {
id: string;
createdAt: Date;
updatedAt: Date;
}
export class InMemoryDatabase<T extends DatabaseRecord> {
private records: Map<string, T> = new Map();
async create(data: Omit<T, 'id' | 'createdAt' | 'updatedAt'>): Promise<T> {
const record = {
...data,
id: crypto.randomUUID(),
createdAt: new Date(),
updatedAt: new Date()
} as T;
this.records.set(record.id, record);
return record;
}
async findById(id: string): Promise<T | null> {
return this.records.get(id) || null;
}
async findAll(): Promise<T[]> {
return Array.from(this.records.values());
}
async update(id: string, data: Partial<T>): Promise<T | null> {
const record = this.records.get(id);
if (!record) return null;
const updated = {
...record,
...data,
updatedAt: new Date()
};
this.records.set(id, updated);
return updated;
}
async delete(id: string): Promise<boolean> {
return this.records.delete(id);
}
}
// src/user.ts
import type { DatabaseRecord } from './database.js';
import { InMemoryDatabase } from './database.js';
export interface User extends DatabaseRecord {
name: string;
email: string;
role: 'admin' | 'user' | 'guest';
}
export class UserRepository extends InMemoryDatabase<User> {
async findByEmail(email: string): Promise<User | null> {
const users = await this.findAll();
return users.find(u => u.email === email) || null;
}
async isAdmin(userId: string): Promise<boolean> {
const user = await this.findById(userId);
return user?.role === 'admin';
}
}
// src/index.ts
import { createServer } from 'http';
import { config } from './config.js';
import { UserRepository } from './user.js';
const userRepo = new UserRepository();
// Create some example users
const admin = await userRepo.create({
name: 'Admin User',
email: 'admin@example.com',
role: 'admin'
});
const regularUser = await userRepo.create({
name: 'Jeff Bruchado',
email: 'jeff@example.com',
role: 'user'
});
console.log('📦 Users created:', { admin, regularUser });
// Simple HTTP server
const server = createServer(async (req, res) => {
const url = new URL(req.url || '/', `http://${req.headers.host}`);
if (url.pathname === '/users' && req.method === 'GET') {
const users = await userRepo.findAll();
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(users, null, 2));
return;
}
if (url.pathname.startsWith('/users/') && req.method === 'GET') {
const id = url.pathname.split('/')[2];
const user = await userRepo.findById(id);
if (!user) {
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'User not found' }));
return;
}
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(user, null, 2));
return;
}
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Route not found' }));
});
server.listen(config.port, config.host, () => {
console.log(`🚀 Server running at http://${config.host}:${config.port}`);
console.log(`📝 Environment: ${config.env}`);
console.log('\nAvailable routes:');
console.log(' GET /users');
console.log(' GET /users/:id');
});Now run:
# Development with hot reload
npm run dev
# Type checking
npm run check
# Production
npm start
Advantages of the Native Approach
1. Faster Startup
No transpilation means faster initialization, especially important for:
- CLI scripts
- Serverless functions
- Unit tests
- Local development
# Startup time comparison
# ts-node (JIT transpilation)
time ts-node src/index.ts
# ~450ms
# Node.js 22+ with strip-types
time node --experimental-strip-types src/index.ts
# ~120ms 🚀2. Minimalist Setup
{
"devDependencies": {
"typescript": "^5.6.0", // Only for type checking
"@types/node": "^22.0.0"
}
}No need for: ts-node, tsx, @swc/core, esbuild, etc.
3. Native Watch Mode
# Node.js built-in watch mode
node --experimental-strip-types --watch src/index.tsDetects changes and restarts automatically, without external tools.
4. Direct Debugging
# Debugging in VS Code works natively
node --experimental-strip-types --inspect src/index.ts
Limitations and Considerations
What DOESN'T Work (Yet)
1. TypeScript Transformations
// ❌ Enums don't work (need transformation)
enum Status {
Active = 'ACTIVE',
Inactive = 'INACTIVE'
}
// ✅ Use union types
type Status = 'ACTIVE' | 'INACTIVE';
// ❌ Decorators don't work
class UserController {
@Get('/users')
getUsers() {}
}
// ✅ Use alternative patterns
const routes = {
getUsers: {
method: 'GET',
path: '/users',
handler: getUsers
}
};2. Namespace Syntax
// ❌ Namespaces are not supported
namespace Utils {
export function formatDate() {}
}
// ✅ Use ES6 modules
export function formatDate() {}3. Optional Parameters in Implemented Interfaces
Works, but be careful with compatibility.
When to Still Use Build Tools
You still need tools like esbuild, swc, or tsc for:
- Production with bundling
- Compatibility with old Node.js
- Using advanced TypeScript features (enums, decorators)
- Bundle size optimizations
// package.json for hybrid setup
{
"scripts": {
"dev": "node --experimental-strip-types --watch src/index.ts",
"build": "tsc",
"start": "node dist/index.js",
"type-check": "tsc --noEmit"
}
}
Comparison: Strip Types vs Alternatives
Node.js Strip Types
# Pros:
# - Zero extra dependencies
# - Fast startup
# - Native watch mode
# - Direct debugging
# Cons:
# - Experimental (may change)
# - No transformations (enums, decorators)
# - Requires Node.js 22+tsx (ts-node Successor)
npm install -D tsx
# Pros:
# - Very fast (uses esbuild)
# - Supports all TS features
# - Works on old Node.js
# - Stable and mature
# Cons:
# - Extra dependency
# - JIT transpilation (small overhead)Bun
# Pros:
# - Native TypeScript without flags
# - Extremely fast
# - Complete runtime
# Cons:
# - Different runtime from Node.js
# - Compatibility may varyDeno
# Pros:
# - TypeScript first-class
# - Secure by default
# - Built-in tooling
# Cons:
# - Different ecosystem
# - Requires code changesThe Future of TypeScript in Node.js
Expected Roadmap
2025-2026:
- Strip types exits experimental
- Support for more TypeScript syntax
- Optimized performance
2027+:
- TypeScript as a first-class citizen
- Possible runtime type checking support (opt-in)
- Complete integration with Node.js tooling
// Future vision (speculative)
const futureNodeTS = {
native_typescript: true,
strip_types: 'stable',
optional_features: {
runtime_type_checking: true, // opt-in
enum_support: true,
decorator_support: true
},
performance: {
startup: 'near-javascript speed',
execution: 'zero overhead',
memory: 'minimal'
},
developer_experience: {
setup_complexity: 'minimal',
tooling_integration: 'seamless',
debugging: 'native'
}
};Industry Adoption
TypeScript is becoming the standard:
- 38.5% of developers already use TypeScript
- Top 5 languages most popular
- Majority of new Node.js projects start with TS
// Emerging pattern in 2025
// Projects start directly in TypeScript
// package.json
{
"name": "my-new-project",
"type": "module",
"scripts": {
"dev": "node --experimental-strip-types --watch src/index.ts",
"build": "tsc",
"start": "node dist/index.js"
}
}
Migrating Existing Project
Step by Step
# 1. Update to Node.js 22+
nvm install 22
nvm use 22
# 2. Update package.json
# Add dev script
# 3. Test direct execution
node --experimental-strip-types src/index.ts
# 4. Check for incompatible features
# - Look for enums
# - Look for namespaces
# - Look for decorators
# 5. Refactor if necessary
# Convert enums to union types, etc.
# 6. Update CI/CD
# Add Node.js 22 to workflowsMigration Script
// scripts/check-compatibility.ts
import { readdir, readFile } from 'fs/promises';
import { join } from 'path';
interface CompatibilityIssue {
file: string;
line: number;
issue: string;
suggestion: string;
}
async function checkFile(filePath: string): Promise<CompatibilityIssue[]> {
const content = await readFile(filePath, 'utf-8');
const lines = content.split('\n');
const issues: CompatibilityIssue[] = [];
lines.forEach((line, index) => {
// Check for enums
if (/^\s*enum\s+/.test(line)) {
issues.push({
file: filePath,
line: index + 1,
issue: 'Enum found',
suggestion: 'Convert to union type or const object'
});
}
// Check for namespaces
if (/^\s*namespace\s+/.test(line)) {
issues.push({
file: filePath,
line: index + 1,
issue: 'Namespace found',
suggestion: 'Convert to ES6 modules'
});
}
// Check for decorators
if (/^\s*@\w+/.test(line)) {
issues.push({
file: filePath,
line: index + 1,
issue: 'Decorator found',
suggestion: 'Use alternative pattern or keep build step'
});
}
});
return issues;
}
async function scanDirectory(dir: string): Promise<CompatibilityIssue[]> {
const entries = await readdir(dir, { withFileTypes: true });
const issues: CompatibilityIssue[] = [];
for (const entry of entries) {
const fullPath = join(dir, entry.name);
if (entry.isDirectory() && entry.name !== 'node_modules') {
issues.push(...await scanDirectory(fullPath));
} else if (entry.isFile() && /\.ts$/.test(entry.name)) {
issues.push(...await checkFile(fullPath));
}
}
return issues;
}
// Execute
const issues = await scanDirectory('./src');
if (issues.length === 0) {
console.log('✅ No compatibility issues found!');
console.log('Your project is ready for --experimental-strip-types');
} else {
console.log(`⚠️ Found ${issues.length} compatibility issues:\n`);
issues.forEach(issue => {
console.log(`${issue.file}:${issue.line}`);
console.log(` Issue: ${issue.issue}`);
console.log(` Suggestion: ${issue.suggestion}\n`);
});
}If you're excited about TypeScript and Node.js news, I recommend checking out another article: Create React App Deprecated: Migrating to Vite and the Future of React where you'll discover another major change in the JavaScript ecosystem in 2025.

