Running TypeScript Natively in Node.js: How We Dropped the Build Step for Scripts
Table of Contents
About the Author

Ahmed Mahmoud
Author & Developer
Software engineer passionate about web development and user experience design.
Founder of Devya · eng-ahmed.com ↗Key takeaways
- Node.js now runs .ts files directly by stripping type annotations at load time. The feature is on by default since Node.js 23.6 and ships in the 22.18 LTS release.
- Type stripping covers only erasable syntax. enum, namespace with runtime code, and constructor parameter properties still need the --experimental-transform-types flag.
- Node.js never type-checks and never reads tsconfig.json. Our type checker remains tsc --noEmit, enforced in CI.
- TypeScript 5.8's erasableSyntaxOnly option turns non-erasable syntax into a compile error, so a file that passes our check is guaranteed to run under Node.js.
- Decorator-based frameworks such as NestJS depend on emitDecoratorMetadata, which type stripping does not provide — our NestJS services kept their build step.
Can Node.js run TypeScript without a build step?
Yes. Since Node.js 22.6 behind the --experimental-strip-types flag, and by default since Node.js 23.6, Node executes TypeScript files by replacing type annotations with whitespace before evaluation. The 22.18 release brought the default-on behavior to the LTS line, and Node.js 24 — the current LTS our team deploys on — runs node script.ts out of the box.
Because stripped types are replaced with blank space rather than removed, line and column numbers in stack traces match the original source exactly, with no source maps involved. At runtime, process.features.typescript reports 'strip', 'transform', or false, which is how our scripts assert the capability before relying on it.
At Devya we moved our internal scripts, database config files, and scheduled jobs to native execution first. Those are the places where a build step hurt the most: every small utility needed either a compile step or a loader dependency such as ts-node or tsx.
What TypeScript syntax does type stripping not support?
Erasable syntax is any TypeScript construct that can be deleted without changing runtime behavior: type annotations, interfaces, type aliases, generics, satisfies, and as casts. Type stripping handles all of it.
Four constructs generate JavaScript code and are therefore not erasable: enum, namespace with runtime values, constructor parameter properties, and import x = require(). Running a file that uses them requires --experimental-transform-types, which switches Node.js from stripping to a full SWC-based transform with source maps.
Our team avoids the transform flag entirely. We replaced enums with const arrays plus derived union types, which are erasable, simpler to serialize, and free of the surprising runtime objects enums generate.
How do we guarantee code stays erasable?
TypeScript 5.8 added the erasableSyntaxOnly compiler option. With it enabled, every non-erasable construct becomes a compile error, so the type checker itself enforces Node.js compatibility. We pair it with verbatimModuleSyntax, which forces type-only imports to be written as import type and prevents a stripped import from changing the module graph.
Two more details matter. Relative imports must include the .ts extension explicitly — Node.js does not rewrite specifiers — so allowImportingTsExtensions belongs in the config. And Node.js itself never reads tsconfig.json: the config file exists for the editor and for tsc, not for the runtime.
Where does type checking happen if Node strips types?
Nowhere at runtime. Node.js discards the types without looking at them, which is the same trade-off esbuild-based tools like tsx made years ago. We run tsc --noEmit as a CI gate and locally in a pre-commit hook, and the editor still surfaces errors through the TypeScript language server. A file with a type error will execute — the gate, not the runtime, is what stops it from reaching production.
Which tool should run TypeScript in 2026?
- Native type stripping: zero dependencies, no type checking, erasable syntax only. Our default for scripts, configs, and scheduled jobs on Node.js 22.18 and newer.
- tsx: an extra dependency, no type checking, supports JSX and tsconfig path aliases. We keep it only where path aliases or older Node.js versions force us to.
- ts-node: a dependency with an optional type-checking mode and slower startup. We no longer add it to new projects.
- tsc build: full type checking and declaration output. Still the only correct answer for published libraries and decorator-heavy applications.
Which projects should keep their build step?
Our NestJS services kept theirs. NestJS dependency injection relies on emitDecoratorMetadata, and neither strip mode nor transform mode emits that metadata, so native execution is not an option there. JSX files are also out of scope, Node.js does not resolve tsconfig path aliases, and packages inside node_modules are never stripped — anything published to npm must ship compiled JavaScript with declaration files.
FAQ
**Q:** Do we still need ts-node or tsx?
**A:** Not for erasable-syntax scripts on Node.js 22.18 or newer. tsx remains useful for JSX, tsconfig path aliases, and older Node.js versions.
**Q:** Does Node.js type-check the files it runs?
**A:** No. Type stripping discards annotations without validating them. Run tsc --noEmit in CI to catch type errors before deployment.
**Q:** Does Node.js read tsconfig.json?
**A:** No. The runtime ignores it entirely, in both strip and transform modes. tsconfig.json configures the editor and tsc only.
**Q:** Can we use TypeScript enums with native execution?
**A:** Only behind --experimental-transform-types. We recommend const object or const array patterns with derived union types instead — they are erasable and behave predictably.
**Q:** Do TypeScript files inside node_modules work?
**A:** No. Node.js deliberately refuses to strip types in node_modules, so published packages must ship JavaScript.
Further Reading
Developer Tooling
Replacing ESLint and Prettier with Biome: Field Notes from Two Production Repos
Biome is a single Rust toolchain that formats and lints JavaScript and TypeScript in one pass, replacing ESLint and Prettier. We migrated two production repositories — here is where it won, what Biome 2.0's type-aware linting changes, and where we still keep ESLint.
Frontend Engineering
AbortController Beyond fetch: Cancellation Patterns We Use in Every React App
Uncancelled fetches do not fail loudly — they resolve late and overwrite fresh state. The AbortController patterns we use in every React app we ship: useEffect cleanup, AbortSignal.timeout(), AbortSignal.any(), and one-signal listener cleanup.