-
Notifications
You must be signed in to change notification settings - Fork 12.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
TypeScript cannot emit valid ES modules due to file extension issue #42151
Comments
They do— import { test } from './importMe.js';
console.log(test); |
My greatest pet-peeve is that automatic TS imports in VSCode are done as |
@RGFTheCoder Yeah, the TS error should be adjusted to say "please use import test from './test.ts' is:
It is highly unintuitive that you need to use a It currently turns valid TS into invalid ES. The compiled output does not execute as expected when following the directions provided by |
Use native ES modules everywhere for consistency between source and tests and to allow the output of TypeScript compilation to function both as input to Rollup packaging and execution by mocha. Node ES module support requires that file extensions are always explicitly specified. TypeScript does not generate the ".js" extension itself but counter-intuitively does allow it to be specified in the original source [1]. [1] microsoft/TypeScript#42151 (comment)
Use native ES modules everywhere for consistency between source and tests and to allow the output of TypeScript compilation to function both as input to Rollup packaging and execution by mocha. Node ES module support requires that file extensions are always explicitly specified. TypeScript does not generate the ".js" extension itself but counter-intuitively does allow it to be specified in the original source [1]. [1] microsoft/TypeScript#42151 (comment)
@RGFTheCoder there’s a setting for that so you don’t have to manually fix up an import for every file: In the future, there will likely be a set of compiler settings that make this the default, and/or make extensionless imports not resolve, but we’re not quite there yet.
Seems reasonable. Not quite with that phrasing, but your PR looks good.
Starting down this path made me realize I failed to label this as a duplicate of #16577. |
I wish this issue could be left open until the problem is solved. There are actual problems with the workaround of just adding .js to the end of the file. Just to name the ones at the top of my mind:
|
@richardkazuomiller I've considered just building this feature out and throwing it on the table. My last PR related to this was merged very quickly, so the compiler will no longer tell you to do something that actually breaks your emitted output (rename I have actually demo'd this logic in my own fork of TSDX and it is working, so I may try to add it directly to the compiler. The fact of the matter is that "CJS |
@andrewbranch I have implemented an algorithm to resolve file extensions AOT. There is no reason theoretically that it could not be optimized and added as an opt-in feature to get valid ESM output. Currently, there is other way of saying it, TypeScript cannot emit valid ES modules. Reopening IssueCould this issue be reopened? It was closed when my #42184 PR was merged, I should probably not have added the "closes" syntax since this issue still exists in emitted output unless you specially craft the source to use absolute imports (which is not theoretically necessary since we are eventually feeding the TS compiler our TS syntax, it could just resolve them AOT on an opt-in basis as shown above). |
It would have been closed as a duplicate of #16577 anyway. This is not the only issue that laments TypeScript’s lack of proper ESM support. The biggest development on that front is #44501 if you’re using ESM in Node, though much of it will be reusable for other ESM targets like modern browsers. The PR will enforce that you write
This will never be the route TS takes. See #15479 (comment), #16577 (comment), #26722 (comment), #33588. |
@andrewbranch Just trying to understand the TypeScript take on ES modules, maybe you can help shed some light. As per me TypeScript compiles to JavaScript that the JavaScript engines can run. I can control the output I want (via tsconfig.json) and then TypeScript will compile accordingly. For example: If I compile for ES3, then TypeScript will re-write my classes to something an ES3 JS engine can run. Then what is different about ES modules? Why it cannot compile it to something that the JS engine can run? I mean it is already modifying my import statements to require calls when I ask it to compile for CJS |
@thetutlage Sadly this team has made it very clear where they land on it—I 100% agree with you, it literally does not emit valid ES modules and as you also pointed out, transpiling is no problem when it comes to backwards compatibility, but when it comes to supporting ES2015 (is really what we're talking about here, just actual functional ES6 imports in emitted output), it is somehow off the table. It has pretty real consequences, as an unfathomable amount of source code is locked into CJS de facto because of that design choice. Some packages, like React IIRC, even ship the "BabelScript" TS emits as the ESM entry point, leaving coercion to actual, valid ES to the downstream consumer. This pointlessly gives up a lot of the benefits that static ES module resolution affords by refusing to rewrite TS-like import specifiers to actual ES import specifiers, and requiring all emitted code to be transpiled, often to CJS, in order to run it (preventing us from having our cake and eating it too). I do not see how there is any cost to adding an opt-in flag so that we can get valid ESM out of the compiler—despite the extensive clarification, I still think it's the wrong call. The standard is five years old and I think output should run when we execute it, even if we have So, I also disagree with the choice, but it seems like ecosystem tools are the way to go for now. See @tszip/tszip which, at its core, mostly just executes the resolution logic we're talking about here over TSC output and rewrites invalid relative directory imports emitted by TypeScript to full file specifiers. I encourage everyone to check the resolution logic in @tszip/resolve-imports regarding this issue. |
This makes me sad. If I shipped a library and said that all projects using that library need a separate plugin whose only job is to append three characters to a string, without which the program can't in any environment, and my library doesnt mention it in the documentation or show any warnings about it, I think nobody would want to use my library 😂 |
Nobody needs to use a separate plugin to rewrite their paths. All module resolution modes allow you to include the
Transforming an import declaration to a require is a simple syntax transformation that doesn’t depend on outside environmental information like what files the compiler found on disk. Transforming a module specifier of
I’m not speaking for the team as a whole on this, but personally I hate this. I think we’d be in a less confusing place today if we made people who want CJS write CJS and people who want ESM write ESM. But that ship sailed long ago. |
Yes, but now you're writing "invalid" TS—it's technically valid because it was added as an ad hoc, so the compiler won't throw, but Nobody thinks to go, "oh let's specify the module with a .js extension even though it doesn't exist¨—the compiler finally tells you to do that after I added it in #42184, but only if you try to reference it via |
Deprecations exist, especially if we get decent support for ESM today in exchange. |
If this is the only way to get valid ESM, then shouldn't all of the TypeScript documentation have |
TypeScript already knows where
If single file modules are supported as first class citizen, then again TypeScript should rewrite that import the way it should be inside a single file. The main point of using the transpiler is not to worry or write code as per the output. Write it in one uniform way and let the config + compiler deal with the rest. What if I want to use TypeScript to output CJS and ESM both. Which standard should I author my code in? |
Also, I think the debate is getting diverged towards specifics. But it is not about specifics, it is more about a generic broader goal of TypeScript. If a compiler claims to output JavaScript from some superset of JavaScript, then it should let the user write code in that superset and not ask them to mix-match JavaScript with superset. |
This is true, but nothing needs to be deprecated, we literally only need optional import specifier rewrites. I appreciate that the TypeScript team has thought about this a lot and has made a decision, but I strongly believe it is the wrong one and it's never too late to make the right call. I have seen the work on Node12 module resolution, and it is fantastic, but it does not solve the problem of allowing maintainers to compile existing projects to "complete ESM" (ES modules which are valid ES programs according to the spec, i.e. refer to absolute import specifiers), without going and rewriting all of their imports to reference Much of today's JS is compiled TypeScript, and therefore locked into the limitations of the TS compiler. As it stands, the compiler is voluntarily incapable of turning today's or yesterday's TypeScript into valid ESM, even if you were willing to go out of your way to ask it to, despite this being feasible in reality. This limitation is imposed on every TS project, and in practice, locks projects into CJS, or this incomplete ESM (which I affectionately call BabelScript, since it must be transformed again before it can be executed). The best we can do is post-processing to coerce that incomplete ESM to full ESM, but everyone who doesn't do that is usually back-transpiling to CJS directly (via Thus, not adding an opt-in flag that lets developers elect to emit valid ESM from normal, legacy TypeScript—as it is written today, not Special Node12 TypeScript with absolute import specifiers ending in .js, but existing libraries with TS-like imports that could be resolved AOT via an opt-in Sorry to Andrew and other team members for repeatedly pressing this issue, I just believe it is possible for us to have our cake and eat it too in this case.
When you're right, you're right—I think the Node12 resolution is a good addition but it basically requires that you trade away TS import specifiers completely, which may be a fine default for Node12, but there's clearly massive utility in letting developers choose. No need to force new defaults on anyone. The "mixing JS and TS" principle is also why it's ultimately insufficient IMO, because it would require you rewriting all the imports in your source (not just TSC rewriting them in emitted output), by hand, in order to get existing codebases to compile to actual ESM. |
Sorry again to drag this out @andrewbranch, but is there a clear reason why catering to Svelte's compiler is appropriate with #44619, but supporting standard ESM as an opt-in flag is ruled out completely? I am struggling to understand the logic here. |
I'm not really sure that comparing an example of continuing the TypeScript-specific |
@orta Till date I am not able to understand when writing TypeScript, should I be writing the code by keeping the compiled target in mind? If yes, then why for years I was writing |
Exactly, could someone explain like I'm 5 why it's OK for TypeScript to convert |
Thanks. I think I understand the technical distinction between the file path and the other parts, and at least some of what makes it difficult to implement, but what I don't understand is the philosophical reason why people don't want to do it. Like for example, I understand that |
Feel free to read up on the links provided in #42151 (comment) Especially #16577 (comment) - but if this thread is just going to keep pulling it back to that topic instead to the actual point of the issue then we'll end up having to lock it also. |
Well, it is your wrong assumption that we haven't read those comments. Infact, it is the other way around. No one from the TypeScript team is yet able to explain the thesis behind this design choice. Atleast, at a high level, tell me in which language am I supposed to author my code. Is it TypeScript or the compile target? Yes, you have all the rights and power to lock issues, but still the question won't be answered. |
I assume you are writing Your argument is about taking a JavaScript language feature As an example, you should note that we don't do CommonJS to UMD, AMD or SystemJS because that's not taking a JavaScript language feature and porting it to an environment - that's the sort of features bundlers have. I'm afraid it is not the same as changing string identifiers in the JavaScript code during emit. |
Okay. So is it safe to assume that JavaScript language features will not be ever modified by TypeScript. For example:
|
Yep, all of that has/will happen. TypeScript follows the JS spec, it doesn't go off and do its own thing anymore - those decisions were made in a very different JS ecosystem. There will be flags for keeping the old behavior when things don't match like decorators and enums, and |
Cool. Thanks for explaining. This makes it easier for me to explain why things are the way things are |
I understand that this is an intentional choice, but I don't think most people do. I just took a quick peek at Discord and people are still asking questions about this several times a month, which is not surprising because of the reasons we've already mentioned. The de facto standard of TypeScript is still to not write the extensions in imports, and the average person doesn't know that they should be adding
I think when I asked why the documentation doesn't have |
That's a great issue to put on the website, but until 4.5 TypeScript didn't support node's esm, so everyone using it ESM with TS happened to have it work through some good luck and that the classic resolver acted very similar to how node's ESM mode worked out of the box. ESM support in node is very new, has been changing a lot and has only recently been stable across most versions is simply the answer. Most people are still writing commonjs in Node though so the docs probably will still represent that, debating about when we should switch is a good question for the website repo. |
Yes. How does this logic not apply to the situation where I write a valid TS import that the compiler will emit as an invalid import specifier? It does not work. It needs to be rewritten and "backported" to the target. It is very straightforward. It seems very obvious that this is a needed feature, which is evidenced by people coming into this issue nearly a year after I brought it up with this team. We are just getting all the downsides of corporate bureaucracy (making a bad design choice and double/triple/quadrupling down for arbitrary, abstract reasons) with none of the upsides (like output that conforms to the 5-year-old specification) here. Worse even, as others mentioned, it is insanely confusing for newer developers (for very good reasons—it should "just work") and has all sorts of downstream impact in terms of CJS lock-in.
It literally is when the import specifier is not valid ECMAScript unless it's rewritten! Polyfills are fine in all other instances except this one apparently. The import specifier in the emitted code will not execute correctly, so the only decision is whether or not it is appropriate to give developers the ability to force rewrites.
Good amount of them are locked into it de facto because of this issue. At this point I'm just going to write the feature and make you guys close the PR. This is ridiculous. |
I welcome thoughtful discussion and debate, but this conversation is running in circles and getting heated. I apologize to those who were just asking questions in good faith, but some others in this thread are going to have to find a more productive way to engage in conversation about decisions they disagree with. This issue is a duplicate of many others, linked throughout the comments above. We have been talking about this for years, and nothing new has been said in this thread, so it has become just a drain on maintainer time and energy. Thank you for understanding. |
Bug Report
This report is based on the problem covered in #28288 and #16577, but intends to emphasize the fact that this explicitly precludes TS from emitting valid ECMAScript modules.
Issue
Because TS imports do not allow file extensions, but imports in emitted ES modules are not resolved to their relevant file extensions (
import ... from './myModule'
vs.import ... from './myModule.js'
), this output is unusable unless transpiled again with Rollup + Babel or a similar toolkit, no matter how much active care is taken while authoring source code. Executing emitted modules will always throwERR_MODULE_NOT_FOUND
because these sources are not rewritten.Switching to
"module": "commonjs"
is not a valid suggestion in this case, as the goal is to get valid ESM output.⏯ Playground Link
Use
yarn test
: https://repl.it/@christiantjl/TSImportFileExtensions💻 Code
tsconfig.json
index.ts
build/index.js
🙁 Actual behavior
The
import { test } from './importMe'
statement is not modified.🙂 Expected behavior
build/index.js
should contain:The text was updated successfully, but these errors were encountered: