Typescript and Modern Node: A Friction Point That Should Not Exist
Node.js natively supports modern ECMAScript modules and TypeScript type stripping, TypeScript must evolve to align with these changes and offer a seamless developer experience.
The JavaScript ecosystem has changed faster than the tools that support it. Since Node 23.6, released on January 7, 2025, the runtime has shipped native type stripping for .ts files by default. Modern Node releases now enforce ECMAScript Module semantics, require explicit file extensions, and support import maps. A developer can write modern TypeScript or JavaScript and run it directly without relying on a bundler or transform pipeline.
This model works because Node follows the ECMAScript specifications closely for module behavior. Imports behave the way the language defines. Modules load the way developers expect. Runtimes no longer need layers of compatibility tooling to compensate for gaps in the platform.
TypeScript, however, has not kept pace with this environment. The friction is visible in every project that tries to run or publish modern ESM code while using TypeScript for type checking.
ECMAScript is the foundation that Node follows
The ECMAScript specification defines how modules import one another and how explicit file extensions must be used. Browsers adhere to these rules. Node adheres to them. Other runtimes such as Deno and Bun follow them even more strictly. The direction is uniform across the ecosystem.
Node’s native support for .ts files brings TypeScript into that world. When the runtime can strip types directly, .ts is no longer a special intermediate format. It is a variation of JavaScript that the platform itself can interpret. This effectively makes ECMAScript, through Node, a primary consumer of TypeScript syntax.
That is the key context. TypeScript is no longer compiling for a hypothetical downstream toolchain. It is compiling for an ECMAScript runtime that enforces modern rules.
TypeScript still expects the pre-ESM world
Despite this shift, TypeScript clings to older assumptions about how imports should look and how Modules should resolve. It still treats explicit extensions as unusual, expects extensionless imports that no longer reflect how modules work, and ignores Node’s import map resolution.
A minimal example illustrates the gap:
Node accepts this. TypeScript flags it unless specific configuration options are enabled. There is no conflict with the ECMAScript specification. There is only a conflict with TypeScript’s historical model.
The rift shows up in builds
The friction intensifies once you want to emit JavaScript for distribution. The natural expectation is simple. Strip types, place the output in a dist directory, and update import paths to point to .js files.
Instead, you often need two tsconfig files. One for development, one for builds. The build configuration exists not because the project is unusual but because TypeScript requires explicit correction to behave like the runtime it targets.
A trimmed example:
These settings do not produce advanced behavior. They produce normal behavior. Without them, the emitted JavaScript contains incorrect module specifiers and cannot run in Node without additional tooling.
Declarations make the problem more obvious. TypeScript emits .d.ts files that reference .ts modules in the output directory, even though only .js files are present. This works only because TypeScript itself is the sole consumer of declaration files and quietly tolerates invalid specifiers.
The runtime does not. When the same patterns appear in emitted JavaScript, they result in failure.
It rejects imports that are valid under the ECMAScript specification and emits artifacts that do not satisfy the runtime’s rules. The behavior is coherent within TypeScript, but those assumptions no longer match the environment through which the code is run. That gap is just inconvenient.
A philosophical stance that no longer fits the ecosystem
TypeScript has long taken the position that it is a type checker first and that it should not rewrite import paths or resolve module aliases. Historically this stance was reasonable. The ecosystem depended on bundlers, loaders, and layered build pipelines. TypeScript was not expected to produce runnable JavaScript on its own.
But the environment changed. Node executes .ts directly. It enforces ECMAScript rules. It supports import maps. Many projects run without bundlers entirely. When the runtime evolves, the tools that target that runtime must evolve too. If they do not, the cost is paid by the developer through extra configuration, extra documentation, and extra ceremony where it should be straightforward.
This is not a theoretical complaint. It is an everyday friction point for engineers who want to publish libraries to npm or maintain a clean non-bundled backend.
Why the friction matters
The cost of this mismatch is visible everywhere:
- Publishing an npm package requires two tsconfig files for cases that should be simple.
- Developers must hand-manage import extensions that the runtime already knows how to interpret.
- Import maps, standardized and supported by modern JavaScript runtimes, do not work reliably with TypeScript’s build pipeline.
- Output artifacts contain references to paths that do not exist.
- Modern ESM workflows, which should be the simplest, become the most fragile.
These issues are not a consequence of adding types to JavaScript. They stem from how TypeScript chooses to interact with modern ECMAScript runtimes.
What reasonable defaults would look like today
The question of breaking existing projects is legitimate. TypeScript has earned a reputation for stability, and projects built on old assumptions should not be forced to migrate.
A practical path looks like this:
- Adopt modern defaults only when a project opts into an ECMAScript aligned mode such as
"module": "nodenext"or a dedicated modern target. - In that mode, allow
.tsextensions in imports without warnings or additional flags. - Rewrite
.tsto.jsin emitted output when targeting Node ESM. - Generate
.d.tsfiles that reference the actual output. - Acknowledge import maps rather than producing code that breaks them.
These changes would not disrupt existing projects. They would improve the developer experience for the growing number of projects written for modern ECMAScript runtimes.
A brief look across the ecosystem
Other runtimes such as Deno and Bun already follow ECMAScript semantics closely. They demonstrate that a predictable, spec-aligned module system is not only possible but practical. The pressure is not coming from one environment. It is the direction of the entire ecosystem.
When the runtime and the compiler disagree about the fundamentals of module semantics, the friction becomes visible in every import statement, every configuration file, and every build artifact.
Final thoughts
TypeScript has delivered enormous value to the industry and elevated the quality of JavaScript codebases. That contribution is not in question. What is changing is the platform beneath it. Writing modern JavaScript should be the default experience, not one guarded by configuration and workarounds.
Node is now a capable ECMAScript runtime with well-defined semantics. As the runtime converges on the language specification, the burden shifts to the tooling around it. The future of TypeScript is not in resisting that convergence, but in aligning with it so cleanly that the distinction fades.