Concerns with TypeScript 4.5's Node 12+ ESM Support · Issue #46452 · microsoft/TypeScript (original) (raw)
For TypeScript 4.5, we've added a new module mode called node12 to better-support running ECMAScript modules in Node.js. Conceptually, the feature is simple - for whatever Node.js does, either match it or overlay something TypeScript-specific that mirrors thes same functionality. This is what TypeScript did for our initial Node.js support (i.e. --moduleResolution node
and --module commonjs
); however, the feature is much more expansive, and over the past few weeks, a few of us have grown a bit concerned about the complexity.
I recently put together a list of user scenarios and possibly useful scripts for a few people on the team to run through, and we found a few sources of concerns.
- Bugs
- UX
- Ecosystem
- User Guidance
Bugs
Most complex software ships with a few bugs. Obviously, we want to avoid them, but the more complex a feature is, the harder it is to cover all the use-cases. As we get closer to our RC date, do we feel confident that what we're shipping has as few blocking bugs as possible?
I would like to say we're close, but the truth is I have no idea. It feels like we'll have to keep trying the features for a bit until we don't run into anything - but we have less than 3 weeks before the RC ships.
Here's a few surprising bugs that need to get fixed before I would feel comfortable shipping node12
in stable.
- Code changes breaking module resolution
- Auto-imports not working: Debug failure on auto-import completion entry details #46332 (technically present in CommonJS)
resolveJsonModule
can't be used withnode12
: Typescript 4.5 resolveJsonModule should also work with node12 and nodenext module and resolveTypes #46362- No errors on extensionless imports from
.ts
and.tsx
files innode12
(unfiled, reported by @andrewbranch) - Strange errors under pnpm (unfiled, reported by @DanielRosenwasser)
package.json
changes in packages not tracked (unfiled, reported by @DanielRosenwasser)
UX Concerns
In addition to bugs we found, there are just several UX concerns. Package authoring is already a source of confusion in the TypeScript ecosystem. It's too easy to accidentally shoot yourself in the foot as a package author, and it's too hard to correctly consume misconfigured packages. The node12
mode makes this a whole lot worse. Two filed examples of user confusion:
- It's too hard to tell whether you're in an ESM or a CJS file: "moduleResolution": "node12" of Typescript 4.5 does not work as expected #46408
- The
export
field is confusing to configure and diagnose: Surprising (or incorrect) priorities with package.json exports fields? #46334 - Poor errors on extensionless imports: Provide better errors on module: node12 and extensionless imports #46152
While there might be a lot of "working as intended" behavior here, the question is not about whether it works, but how it works - how do we tell users when something went wrong. I think the current implementation leaves a lot of room for some polish.
But there are some questions about this behavior, and we've had several questions about whether we can simplify it. One motivating question I have is:
When a user creates a new TypeScript project with this mode, when would they not want "type": "module"
? Why? Should that be required by default?
We've discussed this a bit, and it seems a bit strange that because we want to cover the "mixed mode" case so much, every Node 12+ user will have to avoid this foot-gun.
I would like to see a world where we say "under this mode, .ts
files must be covered by a "type": "module"
". .cts
can do their own CommonJS thing, but they need to be in a .cts
file.
Another motivating question is:
Why would I use node12
today instead of nodenext
?
Node 14.8 added top-level await
, but Node 12 doesn't have it. I think this omission is enough of a wart that starting at Node 12 is the wrong move.
Ecosystem
The ecosystem is CONFUSING here. Here's a taste of what we've found:
- ts-node, Webpack, and Vite don't like imports with
.js
extensions, but TypeScript expects them. Not all of these can be configured with a plugin. - ts-node, Webpack, and Vite, and Deno are fine with
.ts
extensions, but TypeScript doesn't allow them! - Many packages that ship types have started supporting
export
fields, but don't have atypes
sub-field withinexport
(e.g. RxJS, Vue 3). - Many packages have started supporting
export
fields, but their@types
package might not reflect that.
The last two can be easily fixed over time, though it would be nice to have the team pitch in and help prepare a bit here, especially because it's something that affects our tooling for JavaScript users as well (see #46339)
However, the first two are real issues with no obvious solutions that fall within our scope.
There's also other considerations like "what about import maps?" Does TypeScript ever see itself leveraging those in some way, and will package managers ever support generating them?
Guidance
With --moduleResolution node
, it became clear over time that everyone should use this mode. It made sense for Node.js apps/scripts, and it made sense for front-end apps that were going to go through a bundler. Even apps that didn't actually load from node_modules
could take advantage of @types
in a fairly straightforward way.
Now we have an ecosystem mismatch between Node.js and bundlers. No bundler is compatible with this new TypeScript mode (and keep in mind, back-end code also occasionally uses a bundler).
Here's some questions I wouldn't know how to answer confidently:
- Is our guidance to always use this mode for plain Node.js apps, and let tools "catch up"?
- Should new projects that use this mode pick
node12
ornodenext
? - There's a big foot-gun with forgetting
"type": "module"
- should we always recommend.mts
?
Next Steps
I see us having 3 options on the table:
- A mad dash to fix everything - I think this is too hard in the next 2 weeks to pull off.
- Keep the feature in, but make it inaccessible until we're ready - I think temporarily removing the feature would be impractical. It would probably take more work to remove it than to fix the issues I've already mentioned. So we might as well keep it in.
- Ship with some "experimental" labeling - I think this makes the most sense, but with some caveats to what I mean by "ship". It would make sense to just ship this as "experimental", but I think we should make this feature only work in nightly releases so that people can continue to easily use it, but not depend on a stable version until it's ready.