I Deleted ts-node, tsx and 200MB of node_modules: Here’s How Node 24 Runs TypeScript Natively
Last week I did something I hadn’t done in five years of TypeScript development: I deleted ts-node, tsx, tsc-watch and roughly 200MB of…
I Deleted ts-node, tsx and 200MB of node_modules: Here’s How Node 24 Runs TypeScript Natively

Last week I did something I hadn’t done in five years of TypeScript development: I deleted ts-node, tsx, tsc-watch and roughly 200MB of node_modules from a project. I didn't replace them with anything. I just... ran **node app.ts**.
If that sentence doesn’t feel like a revelation, you’ve either been living under a rock, or you haven’t been writing TypeScript long enough to remember the pain. For the rest of us: welcome to Node.js 24.
The Big Shift: Type Stripping, Not Type Compiling
Node 24 runs .ts files directly by stripping type annotations at runtime. No flags. No loaders. No precompile step.
node app.ts // That's it.
// The same applies to the built-in test runner:
node --test test/*.test.ts
But here’s the critical detail most blog posts skim past: Node strips types, it doesn’t compile them. That means anything that requires generating code at compile time simply won’t work.
This is the entire reason type stripping has rules. Let’s walk through them.
The 5 Rules of Type Stripping
1. Use type-only imports
When the stripper sees a regular import, it can’t tell whether the thing is a type or a value. Be explicit:
// Good — type-only import
import type { User, Config } from './types.ts';
import { createUser } from './user.ts';
// Good - inline type imports
import { createUser, type User } from './user.ts';
// Bad - may fail with type stripping
import { User, createUser } from './user.ts';
Pro tip: setting verbatimModuleSyntax: true in tsconfig makes the compiler enforce this for you. More on that in a moment.
2. No enums
Enums compile down to runtime objects. Type stripping can’t produce that. Use a const object plus a derived type:
// Bad
enum Status {
Active = 'active',
Inactive = 'inactive',
}
// Good
const Status = {
Active: 'active',
Inactive: 'inactive',
} as const;
type Status = (typeof Status)[keyof typeof Status];
You get the same autocomplete, the same string values and zero magic.
3. No namespaces
Namespaces also generate runtime code. Use ES modules:
// Bad
namespace Utils {
export function format(s: string): string {
return s.trim();
}
}
// Good
export function format(s: string): string {
return s.trim();
}
If you find yourself reaching for a namespace in 2026, you almost certainly want a regular module.
4. No constructor parameter properties
The public / private shorthand expands into assignments at compile time. Write them out:
// Bad
class User {
constructor(public name: string, private age: number) {}
}
// Good
class User {
name: string;
private age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
}
Three extra lines. Worth it.
5. No legacy decorators
The old experimentalDecorators: true syntax won't work. If you need decorators, use the TC39 stage-3 syntax and check that your Node version actually supports it before relying on it.
Import Paths: Keep the .ts Extension
This one trips people up. Node 24 expects you to import with the .ts extension, not stripped to .js:
import { helper } from './helper.ts';
import type { Config } from './types.ts';
// JSON imports use the import attribute syntax
import config from './config.json' with { type: 'json' };
The compiler rewrites these to .js at build time when you publish. During development, allowImportingTsExtensions lets you keep them as-is.
The tsconfig Setup (Dev vs Build)
You actually need two tsconfigs: one for editing, one for publishing.
For development: tsconfig.json
This config doesn’t emit anything. Node runs your .ts 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"]
}
The four options that actually matter for type stripping:
noEmit: compilation is off; Node runs TS directly.allowImportingTsExtensions: lets you write./helper.tsin imports.verbatimModuleSyntax: forces type-only imports to use thetypekeyword.isolatedModules: rejects features that can't be transpiled file-by-file (the same set type stripping rejects).
For publishing: tsconfig.build.json
When you ship to npm, you need real .js output and .d.ts declarations:
{
"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"]
}
tsc -p tsconfig.build.json
The two new options worth noticing: rewriteRelativeImportExtensions turns ./foo.ts into ./foo.js in the emitted output, and declaration emits .d.ts files so consumers of your package still get types.
Also read I Removed Jest and 80MB of Deps: Testing with Node’s Built-in Runner
The package.json That Ties It Together
{
"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"
}
}
Worth noting:
"type": "module": the package is ESM, matchingmodule: NodeNextand the JSON import attribute syntax.exports: thetypesentry comes first, so TypeScript-aware tooling reads it correctly.prepublishOnly: rebuilds from a clean slate beforenpm publish. You'll never ship staledist/output again.test: uses Node's built-in test runner on.tsfiles directly. No Jest. No Vitest. No config.
I have created a skill for
[typescript-node](https://github.com/manisuec/node-skills)
The Daily Workflow
Because Node strips types without checking them, tsc plays a different role now; it's a type checker, not a compiler.
# One-off type check
tsc --noEmit
# Watch mode while editing
tsc --noEmit --watch
A reasonable dev loop: run tsc --noEmit --watch in one terminal to flag type errors as you edit, and node --test test/*.test.ts in another. When you're ready to publish, npm run build produces the dist/ directory.*
So… Is This Actually Better?
Honestly? Yes.
The mental overhead of ts-node vs tsx vs tsc vs swc vs esbuild was always the worst part of starting a new TypeScript project. Every team had a different combo. Every tutorial assumed a different one. Every CI pipeline had three flags to make TypeScript files run in production.
Node 24 collapses all of that into one path. There’s only one tool, and it’s the one you’re already running.
The catch and there’s always a catch; is that you have to write idiomatic, modern TypeScript. No enums. No namespaces. No parameter properties. No clever compile-time tricks. If you were already doing that, congratulations: you’ve been training for this moment.
If you weren’t… well, you’ve got a migration weekend ahead of you. Worth it.
메타데이터
- post_id
- d10d032abaf6
- slug
- i-deleted-ts-node-tsx-and-200mb-of-node-modules-heres-how-node-24-runs-typescript-natively-d10d032abaf6
- url
- https://medium.com/node-depths/i-deleted-ts-node-tsx-and-200mb-of-node-modules-heres-how-node-24-runs-typescript-natively-d10d032abaf6
- canonical_url
- https://medium.com/node-depths/i-deleted-ts-node-tsx-and-200mb-of-node-modules-heres-how-node-24-runs-typescript-natively-d10d032abaf6
- author_url
- https://medium.com/@manisuec
- status
- ok
- fetched_at
- 2026-06-23 03:48:11