No, you were using non-standard ESM modules (compiled to CommonJS defined by babel)
Typescript recently added support for ESM compatible with node.js see "module": "node16"[1][2]
The Whole ESM saga is clusterfuck, not much better than python 2 -> 3 migration. Large node.js codebases have no viable path to migrate, and most tools still cannot support ESM properly[3]. Stuff is already breaking because prolific library authors are switching to ESM.
As someone that maintain large part of TS/JS tooling in my day job, I absolutely despise decisions made by node.js module team. My side projects are now in Elixir and zig because these communities care about DX.
Yeah it's pretty ugly. This whole thing is a prime example of those cases in which maintainers for mostly arbitrary reasons decide on something and then absolutely ignore all the massive negative feedback they get for this.
They'll cite some nebulous technical reasons of why it has to be this way, but if you offer a PR that actually solves the issue that the community complains about, they'll reject it.
In this case they decided that tsc won't transpile imports. They just did and it's "policy" and it can't be changed. It doesn't matter if it is awful for compatibility, developer experience, etc. It's just the policy. Issues will be closed. And no, even an optional flag to transpile imports is off the table, even if you write the PR for it.
There are many many issues opened related to this in github, but to give an example
Hmm, what is it that Node is doing that’s so bad? I don’t understand why that issue is a big problem.
This explanation in the comments makes sense to me:
Transpilers can add the ability to add extensions at compile time, so a specifier like './file' can be rewritten to './file.js' during compilation along with whatever else is getting converted by the transpiler.
It seems sensible for Node to expect fully-qualified imports, just like a browser would. And (to me) it seems sensible that in a language like TypeScript you should be able to import “foo.ts” and it have it transpiled to the correct filename.
Now, that does not work in TS because they adamantly refuse to modify any of the emitted JavaScript code at all, with no clear explanation except that it’s long-standing policy. Instead they expect you to import “foo.js” in TypeScript, even though that file doesn’t exist until after compilation. That’s a problem, and it seems like it’s caused by the TS team, not Node.
Browsers never required extensions, see https://unpkg.com/
You can load scripts in browser just fine without extension.
> Now, that does not work in TS because they adamantly refuse to modify any of the emitted JavaScript code at all, with no clear explanation except that it’s long-standing policy. Instead they expect you to import “foo.js” in TypeScript, even though that file doesn’t exist until after compilation. That’s a problem, and it seems like it’s caused by the TS team, not Node.
I think they should provide a better explainer. But node.js resolution algorithms is already incredibly complicated, and adding path rewriting to typescript is not going to make it better. There are things like dynamic import and third part libraries. Typescript would need to either analyze whole of project node_modules or bundle custom runtime resolver like webpack breaking compact with deno and friends.
> Hmm, what is it that Node is doing that’s so bad? I don’t understand why that issue is a big problem.
My conclusion is that successful projects without BDFL are prone to corporate takeovers. You have people that working in corporations without writing code and want to make political career as "core" team member of project.
TS can already perform complicated refactorings, such as renaming all imports in a project when you rename a file in vscode. So they definitely have all the information they need already.
Node requiring the .js is more understandable, as that actually affects runtime performance: if the name isn’t fully qualified in source then node needs to stat() more paths during resolution.
> What if typescript is running in deno/bum or wasm?
Every TS project already needs a tsconfig.json to specify what permutation of module system configurations it is using.
> Node requiring the .js is more understandable, as that actually affects runtime performance: if the name isn’t fully qualified in source then node needs to stat() more paths during resolution.
Performance is affected only during startup by maybe few milliseconds. This is a major breaking change to module resolution made by node.js. Why TS should paper over change that was made by node.js.
> TS can already perform complicated refactorings, such as renaming all imports in a project when you rename a file in vscode. So they definitely have all the information they need already.
Some things are supported by tsserver not typescript compiler. Fixing this in TS is only addressing the problem partially and is shifting the problem into tools developers.
The complication here is that Node in part decided on needing fully-qualified imports not just to better align with the browser, but also to use multiple file extensions in a complex signaling process (ETA: which the browser uses mime-types and metadata like type="module" for rather than file extensions): the file extension could be any of .js, .cjs, and .mjs, and then depending on package.json and some other metadata, Node may load the various files in all sorts of different ways.
Typescript decided that they don't always know what output file extension you may need because Node made that logic way too complex and they don't want to just reimplement their own buggy version of Node's loader mechanics but "backwards" in a terrible "guess the file extension" game, so they went with the easiest option which was "user now has to tell us the file extension".
Even if Typescript didn't have a policy to reduce the number of modifications it emits, it's still Node's fault that the file extensions now have three options with an extremely complex dance between them and getting a "guess the file extension" game right would be an extended PITA.
If I’m in a TypeScript file and I import “foo.ts”, why can’t TSC just rewrite that to whatever filename TSC will emit when it compiles foo.ts?
If you’re just using TSC to typecheck and not emitting code (which is what most people actually do in practice), it’s even easier -- just let me import “foo.ts” if that’s the name of a file that exists. The popular bundlers can all handle that just fine.
Because TSC isn't your bundler. It's job is different from a bundler. A bundler runs under the assumption that everything you import it has to find and handle. Typescript only needs to find a definition for an import.
Typescript may have a complete view of a single project, in which case yes, it should know what file type it is emitting in that project, but then it has to track "in project" imports differently from "out of project" imports and needs two different behaviors for those.
All of that gets further complicated by incremental builds and multi-project references and multi-project references with incremental builds.
Which isn't to say that it isn't technically solvable, and maybe "two behaviors" is an alright developer experience even if it would confuse so many new users, it's just that there are a lot of obvious complications in the face of it.
It's also not like they haven't been trying to work on it. Other comments in this thread have pointed to at least one Typescript issue on it. At one point Previews supported a version of this but rather than using the file-type of the current package's emit it relied on the new paired TS file types: .ts => .js, .mts => .mjs, .cts => .cjs. If you imported a ".ts" it always assumed you were importing ".js" and if you imported a ".mts" it always assumed you were importing a ".mjs". There were a lot of complications even with that simple "one experience", but even that experience was terrible, fell down in complications with Node's loader and various bundlers, and had too many bugs. So it was pulled from Previews.
I realise it’s not trivial, but it’s really hard to believe it’s that difficult!
When I look at the multiple(!) issues on TS GitHub asking “please, can we just import .ts files with a .ts extension? That would make life a lot easier”, the comments from the developers pushing back on it aren’t about Node integration issues, they’re about the unshakable principle that TS must never rewrite syntactically valid JavaScript at all.
Make it work within a single project, at least, and leave external projects for later.
Edit to add:
There were a lot of complications even with that simple "one experience", but even that experience was terrible, fell down in complications with Node's loader and various bundlers, and had too many bugs. So it was pulled from Previews.
Do you have a link handy for that discussion? I’d be interested to read it.
> they’re about the unshakable principle that TS must never rewrite syntactically valid JavaScript at all.
Which is starting to be a very useful principle of Typescript. Typescript knows that today it is not your bundler and tries to leave almost all rewriting to your bundler. That leaves bundlers able to strip types without even needing a direct dependency on Typescript. This is why tools like swc and esbuild written entirely in other languages now type strip as well.
This is also why the Typescript team has been a proponent for a proposal to add type stripping (or something like it) to the entire web platform. (There's a Stage 1 proposal in TC-39's process.)
You may not find that useful, but there's a growing ecosystem around "Typescript is just for types, not also for deeper transpilation". It is not just TS developer being "high and mighty" in the face of things you think they could make the developer experience easier on.
The Whole ESM saga is clusterfuck, not much better than python 2 -> 3 migration. Large node.js codebases have no viable path to migrate, and most tools still cannot support ESM properly[3]. Stuff is already breaking because prolific library authors are switching to ESM.
As someone that maintain large part of TS/JS tooling in my day job, I absolutely despise decisions made by node.js module team. My side projects are now in Elixir and zig because these communities care about DX.