· 5 min read
Run TypeScript natively in Node.js with type stripping
Node.js runs .ts files directly with type stripping, now stable. Learn what syntax works, the import rules, and the tsconfig settings to use.

Node.js can run TypeScript files directly: node app.ts works with no compiler, no loader and no flag. It does this with type stripping, which deletes type annotations and runs what's left as plain JavaScript. The feature has been on by default since Node 23.6 and 22.18, and it is marked stable as of Node 25.2 and 24.12.
That makes it a good fit for scripts, CLIs, tooling and small services. It is not a full TypeScript compiler, though, and a handful of rules decide whether your code runs or throws on the first line.
What is Node.js type stripping?
Type stripping means removing the parts of TypeScript that exist only for the type checker, such as annotations, interfaces and type aliases, and leaving the JavaScript underneath untouched. Node does this with a built-in parser before the module is evaluated.
Two details explain most of its behavior:
- Types become whitespace. Node doesn't delete annotations and shift code around. It overwrites them with spaces, so every line and column in your file stays where it was. Stack traces point at the right place without source maps, and Node doesn't generate any.
- There is no type checking. A wrong type is not an error at runtime.
const n: number = "hello"runs fine. You still runtsc(or your editor) to catch type mistakes.
A minimal project looks like this:
// math.ts
export type Point = { x: number; y: number };
export function dist(a: Point, b: Point): number {
return Math.hypot(a.x - b.x, a.y - b.y);
}
// main.ts
import { dist, type Point } from './math.ts';
const a: Point = { x: 0, y: 0 };
const b: Point = { x: 3, y: 4 };
console.log(dist(a, b));
node main.ts
# 5
With "type": "module" in package.json, .ts files are ES modules. Without it they follow the same rules as .js files. You can also use .mts (always ESM) and .cts (always CommonJS).
Which TypeScript syntax does Node support?
Node only runs erasable syntax: TypeScript that disappears cleanly when you remove the types. Annotations, interface, type, generics, as, satisfies, ! non-null assertions and import type all qualify.
Some TypeScript features generate JavaScript code, so they can't be erased. Node rejects them with ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX:
| Syntax | Why it fails | Replacement |
|---|---|---|
enum Color { Red, Green } | Compiles to an object | A const object plus a union type |
constructor(private name: string) | Parameter properties create assignments | Declare the field and assign it |
namespace A { export let x = 1 } | Runtime namespaces emit code | ES modules |
import X = require('x') | Import aliases emit code | import or require() |
| Decorators | Not transformed | A build step, if you need them |
enum Color { Red, Green }
// SyntaxError [ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX]:
// TypeScript enum is not supported in strip-only mode
A namespace that contains only types is fine, since it erases completely. .tsx files are not supported at all.
Earlier releases had an --experimental-transform-types flag that handled enums and similar syntax. It was removed in Node 26. If you depend on that syntax, keep a tool like tsx or a build step.
How do imports work with type stripping?
This is where most first attempts fail. Node resolves modules exactly as written, and it never reads tsconfig.json. So:
Extensions are required. import './math' throws ERR_MODULE_NOT_FOUND. Write import './math.ts'. The same applies to require('./math.ts').
Type-only imports need import type. Node strips each file separately and can't know whether an imported name is a type or a value. If you import a type as a regular name, the stripped JavaScript still asks for it, and the module doesn't export it:
import { dist, Point } from './math.ts';
// SyntaxError: The requested module './math.ts'
// does not provide an export named 'Point'
Mark it as a type, either for the whole import or per name:
import type { Point } from './math.ts';
import { dist, type Point } from './math.ts';
paths aliases don't work. Since Node ignores tsconfig, @/utils style aliases fail. Subpath imports in package.json (names starting with #) are the Node-native replacement.
Nothing under node_modules is stripped. Node deliberately refuses to run TypeScript from dependencies. Packages should still ship JavaScript.
Newer syntax isn't down-leveled. Without tsconfig there is no target, so whatever JavaScript remains must be supported by your Node version. Something like the using declaration runs only on a Node release that implements it.
How should you set up tsconfig for it?
You still want tsc for checking, and you want it to reject the same things Node rejects. The Node docs recommend TypeScript 5.8 or newer with these options:
{
"compilerOptions": {
"noEmit": true,
"target": "esnext",
"module": "nodenext",
"rewriteRelativeImportExtensions": true,
"erasableSyntaxOnly": true,
"verbatimModuleSyntax": true
}
}
What the important ones do:
erasableSyntaxOnlymakestscreport enums, parameter properties and runtime namespaces, so you see the error in your editor instead of at startup.verbatimModuleSyntaxrequiresimport typefor type-only imports, which matches Node's rule.rewriteRelativeImportExtensionslets you write./math.tsin source and havetscemit./math.jsif you later compile for publishing.noEmitis optional. Keep it when you only run.tsfiles directly; drop it when you also build JavaScript output.
A typical workflow is node src/main.ts to run and tsc in CI or a pre-commit hook to type-check. If you ever need to turn stripping off, --no-strip-types disables it.
When should you use it?
It fits well for build scripts, one-off tools, internal CLIs, test files and small services where you control the code and want fewer moving parts. You drop a compile step, a watcher and a dist folder, and stack traces match your source.
Keep a compiler or bundler when you publish packages, need JSX, rely on decorators or enums across a large codebase, use path aliases heavily, or target runtimes older than your syntax.
Key takeaways
node file.tsruns TypeScript by replacing types with whitespace; it's stable as of Node 25.2 and 24.12.- There is no type checking at runtime. Keep
tscfor that. - Only erasable syntax works: no enums, parameter properties, runtime namespaces or decorators.
- Import specifiers need the
.tsextension, and type-only imports needimport type. - Turn on
erasableSyntaxOnlyandverbatimModuleSyntaxsotsccatches what Node would reject.
FAQ
Does Node.js type-check my TypeScript?
No. It only removes types. A type error that tsc would report still runs, so type-checking stays a separate step.
Why does my enum throw an error in Node?
Enums compile to a runtime object, which isn't erasable. Replace it with a const object and a union type, or keep a build tool for that file.
Do I need source maps with Node type stripping?
No. Types are replaced with whitespace, so line and column numbers in errors already match your .ts file.