Node.js 22.6 runs TypeScript by removing erasable syntax
Node.js adds experimental type stripping without type checking or tsconfig interpretation. The release draws a specific boundary around annotations that can disappear without generating new JavaScript behavior.
Some TypeScript syntax describes a program without adding runtime behavior. A parameter annotation can disappear while the function remains ordinary JavaScript. Other constructs need generated code to preserve their meaning. Node.js 22.6, released August 6, draws its initial built-in TypeScript support along that distinction. The experimental loader removes inline types and executes the remaining source without performing a type check.
The flag is --experimental-strip-types. Its scope is deliberately smaller than running the TypeScript compiler. Enums and other constructs requiring transformation remain unsupported. The documentation in the release tag also excludes interpreting tsconfig.json, rewriting module systems, and compiling newer JavaScript syntax for older engines. Those decisions define the new execution path's contract.
I have reservations about TypeScript, but its ecosystem makes it useful for building web applications. Running a small typed program without arranging another execution dependency is appealing. That convenience is useful only when the supported subset is clear. A program designed for a framework's compiler pipeline can depend on transformations that removing annotations cannot reproduce.
Whitespace preserves source positions
The release documentation says Node replaces inline annotations with whitespace. Keeping the positions of the remaining source avoids requiring a generated source map solely to recover correct line numbers in stack traces. This is a specific advantage of stripping over transformations that insert or substantially rearrange JavaScript. The implementation can preserve location information because the removed syntax contributes no executed operation. A small example fits that model:
function message(name: string): string {
return "Hello, " + name;
}
console.log(message("reader"));
Saved as greeting.ts, the file runs with node --experimental-strip-types greeting.ts. The annotations describe the function for a checker. Removing them leaves its parameter, return statement, and call intact. Node does not need to choose a new runtime representation for the declared string type. It simply executes JavaScript after the annotation locations have been replaced.
That also explains why successful execution is not evidence that the file passed TypeScript's checks. Node explicitly performs no type checking in this mode. A type mismatch that still leaves executable JavaScript can reach runtime. The ability to remove annotations and the ability to establish that the annotations agree with the program are separate operations.
This separation changes what a successful command establishes. It shows that Node could load and execute the selected source along the path exercised by that run. It does not establish that an uncalled function satisfies its annotations or that a different input follows a valid typed operation. A separate compiler check can examine relationships beyond the execution observed in one invocation.
For a script author, keeping those results distinct avoids assigning a new meaning to annotations. They remain information for tooling, even when the runtime accepts their syntax. The stripping loader does not turn a declared string into a runtime string conversion. A value reaches the JavaScript operation according to the program's executed code, so validation of external data still needs an actual operation in that code.
Some TypeScript constructs cannot simply disappear
The initial documentation identifies enums, namespaces, experimental decorators, and parameter properties as unsupported. Their presence can require JavaScript generation or interpretation beyond removal. A constructor parameter property, for example, asks a TypeScript compiler to connect a parameter to an instance property. Stripping its type does not supply the corresponding runtime transformation.
This is why the initial feature should be understood through syntax with erasable meaning. Supporting one .ts file does not establish support for every language feature permitted in another .ts file. The extension identifies the input category, while the constructs inside determine whether stripping is sufficient. The loader rejects unsupported transformations instead of trying to infer a project's compiler choices.
The distinction can guide source intended for this execution model. Ordinary JavaScript class operations with type annotations retain their behavior after erasure. TypeScript-specific features that produce JavaScript need a fuller tool. That choice is about what the source requires, not a general judgment that one project style is suitable for every application.
Module format remains a Node.js decision
The loader does not convert CommonJS into ES modules or reverse that direction. The release-tagged documentation says .ts follows the module-system rules used for .js. A nearby package.json with "type": "module" establishes ES module interpretation for that case. .mts always selects ES modules, while .cts always selects CommonJS. .tsx is unsupported in this initial implementation.
These rules keep runtime module interpretation separate from the presence of type syntax. A source file still needs to express the module operations appropriate to its execution format. A tsconfig.json setting that would cause tsc to emit a different format does not change Node's stripping behavior. The runtime does not consult that configuration to reproduce the compiler's output.
Explicit file extensions are required in relative imports and require calls. The documentation illustrates using ./file.ts directly. That is a meaningful change from workflows where a compiler or bundler resolves a source reference and rewrites the emitted path. In stripping mode, the import names the file Node must load, so the written path needs to be suitable for that operation.
TypeScript's allowImportingTsExtensions option can permit checking source that uses those paths. This addresses the checker side of the relationship. It does not ask Node to read the compiler option. The two tools can agree on the source through compatible settings while retaining separate responsibilities for checking and execution.
Type imports must identify themselves
Removing an interface declaration does not automatically explain whether a named import was intended to exist at runtime. The initial Node documentation therefore requires the type keyword for imports used only as types. Without that marker, Node treats the import as a value import. A module that exports no corresponding runtime value can then fail during loading. An illustrative pair of modules makes the intent explicit. No runtime value is required from the first file, model.ts, which declares only a type:
export interface Label {
text: string;
}
The consumer names the source extension directly and marks the imported interface as a type. Its main.mts file then creates an ordinary JavaScript object, with a local annotation that can also be removed. No runtime export named Label is required by this source:
import type { Label } from "./model.ts";
const label: Label = { text: "ready" };
console.log(label.text);
The import can disappear because its syntax identifies its role. The object and property access remain runtime operations. This makes the separation visible without asking Node to run type inference across the imported module. The documentation recommends verbatimModuleSyntax to align the TypeScript checker's import behavior with this requirement.
Project configuration does not become runtime configuration
The release intentionally ignores path aliases and other behavior driven by tsconfig.json. A source reference that depends on a compiler-only alias consequently needs another execution arrangement. Removing annotations cannot make an alias meaningful to Node's module resolver. This prevents the lightweight path from silently becoming a partial, configuration-sensitive implementation of the TypeScript compiler.
The same limit applies to JavaScript target conversion. A configured TypeScript target can guide another tool to transform source for an older environment. Node's stripping mode executes the remaining JavaScript using its own engine. Runtime syntax support follows that engine, so a project cannot rely on an ignored target setting to create a compatibility transformation.
Input forms are limited too. The versioned documentation supports type stripping with --eval and uses --input-type to determine its module system. It excludes TypeScript syntax from the REPL, standard input, --print, --check, and inspect. These are release-day limits that matter when a command-line workflow assumes all Node input paths treat typed source alike.
Dependency publication keeps a stricter boundary
Node refuses to strip TypeScript under node_modules by default. The documentation presents this as discouraging publication of packages requiring this runtime handling. Direct execution of an application's typed source is therefore separate from deciding the distribution format expected of its dependencies. A package consumer should not infer that this experiment establishes a new universal publication convention.
The documentation points users requiring full syntax and compiler-configuration support toward third-party execution tooling. That remains a coherent choice for applications depending on transforms. Type stripping provides a smaller path for source that does not need them. A project can choose the path by identifying the behavior its source actually requires.
I regard the explicit limits as the most useful part of this first release. It makes a typed JavaScript subset executable while leaving checking and code generation to tools designed for those jobs. The whitespace approach also preserves a simple relationship between executed code and source locations. For a program using erasable annotations, explicit module paths, and type-only imports, the new flag removes an execution step without claiming to reproduce an arbitrary TypeScript build.