Node.js 24 brings something developers have wanted for years: the ability to run TypeScript files directly without any extra tooling. No ts-node, no tsx, no custom loaders, and no bundlers required for basic execution. If you’re new to what Node.js 24 offers overall, check out our complete Node.js 24 breakdown first.

You can now simply run:
node app.ts
// the built-in test runner can be run the same way:
node --test test/*.test.ts
This is a significant milestone. However, understanding exactly how Node.js handles TypeScript is crucial to avoiding frustration. Under the hood, Node.js does not compile your TypeScript code in the traditional sense. Instead, it performs type stripping, it removes type annotations and related syntax at runtime, leaving behind clean JavaScript.
This approach is fast and lightweight, but it comes with limitations. Features that rely on code generation during compilation simply won’t work. Let’s break this down clearly.
How Type Stripping Actually Works
When Node.js encounters a .ts file, it parses the code, strips away everything related to types, and executes the remaining JavaScript. There is no type checking at runtime, and no transformation that generates additional code. This design choice prioritizes performance and simplicity. It works beautifully for most modern TypeScript code, but you need to be aware of the five major features that don’t survive the stripping process.
The Five TypeScript Features You Can’t Use with Native Stripping
1. Type-Only Imports Without Proper Marking
If you import something that exists only as a type, Node.js needs to know it can safely remove the entire import. Always use the type keyword for pure type imports.
Recommended patterns:
import type { User, Config } from './types.ts';
// Mixed import with inline type
import { createUser, type User } from './user.ts';
Avoid this:
// Risky — may cause issues during stripping
import { User, createUser } from './user.ts';
To make this consistent across your project, enable verbatimModuleSyntax: true in your tsconfig.json. TypeScript will then enforce proper import syntax during type checking.
2. Enums
Traditional enums generate runtime objects, which type stripping cannot produce.
Not supported:
enum Status {
Active = 'active',
Inactive = 'inactive',
}
Modern alternative:
const Status = {
Active: 'active',
Inactive: 'inactive',
} as const;
type Status = (typeof Status)[keyof typeof Status];
This pattern gives you both a runtime object and a derived type with zero compilation overhead.
3. Namespaces
Namespaces also generate runtime code. In modern applications, ES modules are the better choice anyway.
Avoid:
namespace Utils {
export function format(s: string): string {
return s.trim();
}
}
Preferred:
export function format(s: string): string {
return s.trim();
}
This aligns perfectly with native ESM support in Node.js. If you’re migrating an older codebase to ESM, our AMD/UMD to ESM migration guide walks through the process step by step.
4. Constructor Parameter Properties
The convenient shorthand for declaring and initializing properties in constructors requires code generation. Not supported:
class User {
constructor(public name: string, private age: number) {}
}
Explicit version:
class User {
name: string;
private age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
}
While slightly more verbose, this pattern is explicit and works seamlessly with type stripping.
5. Legacy Decorators
The experimental decorator syntax (enabled via experimentalDecorators: true) is not supported. If you need decorators, consider the current TC39 stage 3 proposal, but verify support in your target Node.js version first.
Import Paths: Keep the .ts Extension
Another important change is how you handle imports. In files that run directly with Node.js, you must include the .ts extension (for more on async module patterns, see our top-level await guide):
import { helper } from './helper.ts';
import type { Config } from './types.ts';
For JSON files, use the import attribute syntax:
import config from './config.json' with { type: 'json' };
This requirement might feel unfamiliar at first, but TypeScript’s rewriteRelativeImportExtensions option makes it easy to handle when building for distribution.
Managing Two tsconfig Files
With native TypeScript support, your development and production setups have different needs. The cleanest approach is to maintain two configuration files. For deeper tsconfig recipes across different stacks, see our practical tsconfig guide. Also check TypeScript 6’s breaking changes if you’re on the latest compiler version.
Development Configuration (tsconfig.json)
This config focuses purely on type checking. No emission happens because Node.js runs the source files directly.
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"noEmit": true,
"resolveJsonModule": true,
"isolatedModules": true,
"verbatimModuleSyntax": true,
"allowImportingTsExtensions": true,
"lib": ["ES2022"],
"types": ["node"]
},
"include": ["src/**/*.ts", "test/**/*.ts"],
"exclude": ["node_modules"]
}
Key settings:
- noEmit: Prevents unnecessary JavaScript output during development.
- allowImportingTsExtensions: Allows
.tsin import paths. - verbatimModuleSyntax: Enforces correct type import syntax.
- isolatedModules: Ensures compatibility with per-file processing.
Build Configuration (tsconfig.build.json)
Use this when preparing your package for distribution.
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"outDir": "dist",
"rootDir": "src",
"resolveJsonModule": true,
"isolatedModules": true,
"verbatimModuleSyntax": true,
"allowImportingTsExtensions": true,
"rewriteRelativeImportExtensions": true,
"lib": ["ES2022"],
"types": ["node"]
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "test"]
}
The build command is simple:
tsc -p tsconfig.build.json
Package Configuration for ESM
Here’s a solid package.json setup for a modern ESM package targeting Node.js 24+:
{
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"files": ["dist", "README.md", "LICENSE"],
"scripts": {
"build": "tsc -p tsconfig.build.json",
"clean": "rm -rf dist",
"prepublishOnly": "npm run clean && npm run build",
"test": "node --test test/*.test.ts",
"typecheck": "tsc --noEmit"
},
"engines": {
"node": ">=24.0.0"
}
}
This configuration ensures proper dual support for types and runtime code, with clean build automation. Use NVM and .nvmrc to pin the Node.js version across your team.
Recommended Development Workflow
With these pieces in place, your daily workflow becomes straightforward:
- Edit your TypeScript files.
- Run type checking in watch mode:
tsc --noEmit --watch - Run tests directly:
node --test test/*.test.ts - When ready to publish:
npm run build
This setup gives you the speed and simplicity of running TypeScript natively while maintaining strong type safety through tsc. For a comprehensive TypeScript best practices guide covering project structure, error handling, and production patterns, check out our dedicated deep dive.
Final Thoughts
Node.js 24’s native TypeScript support represents a major step forward in reducing complexity in the JavaScript ecosystem. By understanding type stripping and respecting its boundaries, you can eliminate several development dependencies while keeping a robust, maintainable workflow.
The key is embracing modern TypeScript patterns that align with runtime execution. Skip legacy features that require heavy transformation, keep your imports explicit, and let the TypeScript compiler focus on what it does best: checking types.
Many developers are already removing ts-node, tsx and related tools from their stacks. The result is lighter, faster projects with fewer moving parts. If you want to understand the performance gains at the engine level, our V8 JIT optimization techniques article explains how V8 compiles and optimizes your JavaScript under the hood.
What are your thoughts on this change? Have you started experimenting with native TypeScript execution in Node.js 24 yet? I’d love to hear about your experience in the comments.