Run TypeScript Directly in Node.js with Type Stripping
Configure Node.js native TypeScript execution, use explicit type imports and file extensions, and recognize when compilation is still required.
Current Node.js can run .ts, .mts, and .cts files by removing erasable TypeScript syntax before execution. It does not type-check, read tsconfig.json, transform module formats, or support .tsx. Treat node script.ts as a lightweight execution path for type-annotated JavaScript, not as a replacement for tsc or a full TypeScript runner.
Built-in type stripping arrived in Node.js 22.6. It became enabled by default in 22.18 and 23.6, stopped showing an experimental warning in 22.18 and 24.3, and became stable in 24.12 and 25.2. This article targets the stable behavior documented for Node.js 26.7.
Run syntax that can be erased
Types, interfaces, and type-only imports can disappear without generating new JavaScript:
// greet.ts
interface User {
name: string;
}
function greet(user: User): string {
return `Hello, ${user.name}`;
}
console.log(greet({ name: 'Ada' }));
Run it directly:
node greet.ts
The result is Hello, Ada. Node replaces inline types with whitespace, which preserves line positions for stack traces without creating source maps. It performs no static check, so a type error does not by itself stop execution:
function double(value: number): number {
return value * 2;
}
console.log(double('6')); // TypeScript error, but JavaScript prints 12
Run tsc --noEmit separately when correctness depends on the types. The Node.js 26 TypeScript documentation explicitly separates type stripping from type-checking and full TypeScript support.
Match Node’s module and extension rules
Node determines the module system for .ts the same way it does for .js. The nearest package.json with "type": "module" makes .ts an ES module; .mts is always ESM, and .cts is always CommonJS. Node does not convert one module system into the other.
For the following ESM example, put "type": "module" in the nearest package.json. Relative imports then need the runtime extension:
// format.ts
export type FormatOptions = { uppercase?: boolean };
export function format(
value: string,
options: FormatOptions = {},
): string {
return options.uppercase ? value.toUpperCase() : value;
}
// main.ts
import { format, type FormatOptions } from './format.ts';
const options: FormatOptions = { uppercase: true };
console.log(format('ready', options));
import './format' does not gain an implicit .ts extension. The official module-system rules for stripped TypeScript require explicit extensions and document the .ts, .mts, and .cts behavior.
If the same source will later be emitted as JavaScript, TypeScript 5.7 and newer can rewrite relative .ts extensions with rewriteRelativeImportExtensions. Decide whether the files are execution-only scripts or distributable sources before setting noEmit.
Mark type imports explicitly
Node does not ask the TypeScript checker whether an imported name is a type. Without the type modifier, it treats the specifier as a runtime import:
import type { FormatOptions } from './format.ts';
import { format, type FormatOptions as Options } from './format.ts';
Both forms let Node erase the type imports. By contrast, import { FormatOptions } asks the runtime module for a value that it does not export and fails during module loading. verbatimModuleSyntax makes TypeScript check code using the same explicit distinction.
Use a checker configuration that models stripping
Node recommends TypeScript 5.8 or newer and this baseline for files meant to run directly:
{
"compilerOptions": {
"noEmit": true,
"target": "esnext",
"module": "nodenext",
"rewriteRelativeImportExtensions": true,
"erasableSyntaxOnly": true,
"verbatimModuleSyntax": true
}
}
erasableSyntaxOnly reports TypeScript constructs that need JavaScript generation. verbatimModuleSyntax preserves the distinction between value and type imports. module: "nodenext" makes the checker model Node’s module rules. Node itself ignores this file; the configuration makes a separate tsc --noEmit run agree more closely with the runtime.
The noEmit setting is appropriate for scripts executed only as TypeScript. Omit it when a library or application still needs distributable .js output.
Know what type stripping cannot do
Node rejects syntax that requires a transform, including enums, parameter properties, import aliases, and namespaces containing runtime values. It also rejects decorators because the runtime does not implement that JavaScript syntax, and .tsx is unsupported. These limitations are listed in Node’s TypeScript features table.
Node also ignores paths, target, JSX settings, and every other tsconfig.json behavior. A paths alias may satisfy the checker while failing at runtime; package imports entries beginning with # are the closest native alternative. Type stripping is intentionally disabled for TypeScript files under node_modules, so publishing raw .ts dependencies is not a portable package strategy.
Use direct execution for small tools, configuration scripts, tests, and services whose source already uses erasable syntax and current JavaScript features. Use tsc, tsx, or another full transformer when you need JSX, decorators, enums, downlevel output, path transformation, source maps for transformed code, or a different TypeScript version’s complete syntax. In either case, keep type-checking as an explicit build or test step.