Typescript: Provide a way to add the '.js' file extension to the end of module specifiers

Created on 16 Jun 2017  ·  273Comments  ·  Source: microsoft/TypeScript

In order to use es6 modules in the browser, you need a .js file extension. However output doesn't add it.

In ts:
import { ModalBackground } from './ModalBackground';
In ES2015 output:
import { ModalBackground } from './ModalBackground';

Ideally I would like this to be output
import { ModalBackground } from './ModalBackground.js';

That way I can use the output in Chrome 51

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>Webpack boilerplate</title>
  <script type="module" src="index.js"></script>
</head>
<body></body>
</html>

image

Related to https://github.com/Microsoft/TypeScript/issues/13422

ES Modules Needs Proposal Suggestion

Most helpful comment

Also, to generalize this issue a bit, I don't think it's actually about adding a .js extension, but resolving the module specifier to an actual path, whatever the extension is.

All 273 comments

It's not just related to #13422, it's the same issue. But responses have been quite negatives, despite the fact I think it's a important issue, so hope your issue will be better received.

Well, I hope its added, we were really looking forward to discussing my POC using this in my next TypeScript podcast, but looks like we will have to wait to use TypeScript with no build tools.

At the moment TypeScript doesn't rewrite paths. It's definitely annoying, but you can currently add the .js extension yourself.

@justinfagnani @rictic

Thanks for the tip, I'll write a shell/node script to do this.

@DanielRosenwasser would it make sense to collect the native ES6 module issues under a label?

Also, to generalize this issue a bit, I don't think it's actually about adding a .js extension, but resolving the module specifier to an actual path, whatever the extension is.

I've come across another issue which isn't really the domain of typescript but it's for my use case.

I'm not sure how to handle node_modules. Normally webpack bundles them into the code via the ts-loader but obviously, this is not understood by the browser:

import { KeyCodes } from 'vanilla-typescript;
https://github.com/quantumjs/vanilla-typescript/blob/master/events/KeyCodes.ts#L3

Adding a js extension here is meaningless.

I guess there would have to be a path expansion by typescript or a url resolver running on the server.

I appreciate its a rather niche case, but I think it would be a way TS could shine early in this area. Maybe it could be a plugin to the tsc compiler?

For anyone coming to this and wants an interim solution I wrote a script to add a js file extension to import statements:

"use strict";

const FileHound = require('filehound');
const fs = require('fs');
const path = require('path');

const files = FileHound.create()
  .paths(__dirname + '/browserLoading')
  .discard('node_modules')
  .ext('js')
  .find();


files.then((filePaths) => {

  filePaths.forEach((filepath) => {
    fs.readFile(filepath, 'utf8', (err, data) => {


      if (!data.match(/import .* from/g)) {
        return
      }
      let newData = data.replace(/(import .* from\s+['"])(.*)(?=['"])/g, '$1$2.js')
      if (err) throw err;

      console.log(`writing to ${filepath}`)
      fs.writeFile(filepath, newData, function (err) {
        if (err) {
          throw err;
        }
        console.log('complete');
      });
    })

  })
});

I might make this into a cli tool..

@justinfagnani's comment hits the nail on the head.

Also, to generalize this issue a bit, I don't think it's actually about adding a .js extension, but resolving the module specifier to an actual path, whatever the extension is.

when you write

import { KeyCodes } from 'vanilla-typescript';

or for that matter

import { KeyCodes } from 'vanilla-javascript';

you are importing from an module specifier, it may or may not be a file but adding .js to the end in this case is not likely to result in a valid resolution.

If you are writing a NodeJS application then the NodeJS Require algorithm will attempt various resolutions but it will likely _not_ attempt to resolve it to 'vanilla-typescript.js' because it references an abstract name and will, by convention and by configuration, be resolved (perhaps over various attempts) to something like '../../../node_modules/vanilla_typescript/index.js'.

Other environments, such as AMD have differences as to how they perform this resolution but one thing that all of these environments have in common is some notion of an abstracted module specifier.

I bring this up because the ES Module implementations shipping in various browsers implement something that is incomplete. If we consider even our simplest dependencies, and as soon as we broach the subject of transitive ones, it becomes clear that there will need to be a way to configure the doggone thing.

That may be far off, but as you are discovering, it is not realistic to write to this (politely) proof of concept implementation we have been given.

Furthermore, I do not see how TypeScript could possibly help here since the issue is environment specific.

@QuantumInformation your program for adding .js to paths looks nice, lightweight, elegant even, but you are ultimately implementing your own module bundler. That is fun and interesting work but it demonstrates the deficiencies in the current implementations available in browsers. Even if you write in pure JavaScript, you still need something to compile and package your transitively imported dependencies.

I am basically just ranting about the fact that the implementation of ES Modules that was released is tremendously far from adequate.

Again NodeJS, RequireJS AMD, Dojo AMD, Sea Package Manager, CommonJS, Browserify, Webpack, SystemJS, all have their own differing ways of doing things but they all provide abstract name resolution. They have to provide it because it is _fundamental_.

Thank you for reading my rant.

Not sure which version of TS added it, but imports such as './file.js' now work (even if the file is actually file.ts).
TypeScript resolves the file fine, and outputs the complete .js import to the target.
lit-html use it: https://github.com/PolymerLabs/lit-html/blob/master/src/lib/repeat.ts#L15

It's possible since TS 2.0. But tools like webpack don't support it so in the end it's useless.

It's useless if one is using ts-loader on sources (the most common use-case).
It is still possible to bundle the target (usually "dist" folder), as the actual js file exists there and can be found by the resolution process.

I wonder if I could implement a quick transformation in ts-loader that strips .js extensions from the target code, allowing one to bundle directly from sources.

Feel free to do so, that would be great. I posted the issue on main webpack ts loaders, like ts-loader, a few months ago, and I've been quite badly received...

For information, there is no problem with the rollup typescript plugin, as a proof it's doable.

I fail to see what good this does until browser loader implementations and the WGATWG loader spec support at least _some_ configuration because most dependencies won't load correctly.

From my point of view, none of this matters until it is practical to use the native loader on an import that refers to an arbitrary string literal specifier, something that may not yet be a URL, and have that go through a transformation that yields the actual URL.

Until then we will remain dependent on tools like SystemJS and Webpack.

I created a tiny transformer that strips the '.js' from import/export statements.
I used tsutils type guards, so yarn add tsutils --dev. (the package is usually installed anyway if you have tslint in your project, so so extra dependency)

https://gist.github.com/AviVahl/40e031bd72c7264890f349020d04130a

Using this, one can bundle ts files that contain imports from files that end with .js (using webpack and ts-loader), and still transpile sources to esm modules that can load in the browser (using tsc).

There is probably a limited number of use-cases where this is useful.

EDIT: I updated the gist to work with exports as well. it's naive and not optimized, but works.

Any movement on this issue?

This matter of extension take us back to the very begining of TypeScript and why a tsconfig.json was needed and why a module option was added to the compilerOptions setting.

Since that matter of extension of extension matters only for ES2015+ as require is able to resolve quite well, let it be added by the compiler when targeted code is ES2015+.

  1. .js for .ts
  2. .jsx for .tsx

Hello, I'm coming at this late but would like to help. I am having trouble understanding what the issue is here. From the OP example it is:

import { ModalBackground } from './ModalBackground';

Is the issue that we don't know what './ModalBackground' is? It could be a folder or something else?

If we run tsc on the entire project and we know that ModalBackground.ts exists, then we would know that it is safe to add the extension, no?

This issue is also something the RxJS community is very interested in. What is the timeline on a solution for this? Is it even prioritized? Are there any third party transformations that would help?

I'm not really sure if this is a problem if the output target is ES2015 is it? This could maybe fall into the domain of a ES2015browser capability. Even more so, @justinfagnani can't we push for this as a platform goal to worry about? (Maybe need to fork into separate thread).

Even more so, @justinfagnani can't we push for this as a platform goal to worry about? (Maybe need to fork into separate thread).

Yes, of course lots of people want to have the platform support some form of bare-specifiers, but the current fact is that it doesn't and there's no proposal even to add them. We need to go through that whole, likely multi-year, process.

Even if we eventually get bare-specifier support, it's incredibly unlikely that it would be in the form of node-module-resolution as-is. So there's going to be a mismatch between the resolution algorithm used by tsc to find files, and the resolution algorithm used by browsers (and possibly even node's native module support, afaik).

It'd be great if tsc could reify what path it used to find another module so that downstream tools and environments don't interpret specifiers with a conflicting opinion.

@justinfagnani they are already interpreted with a conflicting opinion. TS produces a JS file that the JS code it produces does not point to. ES6 is the closest thing to an officially correct way to do JavaScript, so if the ES6 code produced by TypesScript is wrong, this is a bug plain and simple. There is no need to wait for any proprosals and such, just fix TypeScript's bug. But today people feel that if they don't find fault with something and put it through 10 layers of proposal before acting, then they are not acting intellectually. Give me a break.

@aluanhaddad It's true that many projects won't benefit, but there are some projects which don't use npm dependencies (or which are capable of handling the npm dependencies in some way), so those projects would benefit.

It's also very useful for TypeScript libraries, which are compiled to ES6. Right now those libraries cannot be used natively in the browser, but if TypeScript outputted a .js extension then they would work.

@justinfagnani It's not standardized or implemented yet, but there is a proposal to make npm packages work in the browser.

The package name map can be automatically generated by the TypeScript compiler, or by another tool.

So I am relooking at this again, was there any nice workaround to this, apart from my node script?

I've been using this solution:
https://github.com/Microsoft/TypeScript/issues/16577#issuecomment-343610106

But I believe if module does not have the file extension but is served with the correct MIME type, that should resolve.

Any movement on this?

@Kingwl Will you support some other file extensions? such as .mjs .es .esm.

maybe not, this is another feature

How is this even a thing? The typescript compiler _knows_ that the target output is a JS file. I've been browsing these threads for 15 minutes and I still don't understand why it omits the extension.

By the number of references to this issue, I assume that it's moving in the right direction. I'll have another try soon.

How is this even a thing? The typescript compiler _knows_ that the target output is a JS file. I've been browsing these threads for 15 minutes and I still don't understand why it omits the extension.

There are common use cases, which people use, where the lack of extension allows for a more flexible infrastructure. Node require hook and webpack loader are two such cases.

How is this even a thing? The typescript compiler _knows_ that the target output is a JS file. I've been browsing these threads for 15 minutes and I still don't understand why it omits the extension.

There are common use cases, which people use, where the lack of extension allows for a more flexible infrastructure. Node require hook and webpack loader are two such cases.

None of which the browser modules care about.

Would it just kill the typescript team to add an opt-in flag to emit a .js extension? We _do_ know what we're doing here, or there wouldn't be a dozen threads (both open and closed) still garnering replies and confused questions. We understand it's not a deficiency with TS, but if TS is here to solve our JS woes, then add this woe to the list please.

DISCLAIMER:

Yes, I understand this may lead to a ton of people posting issues here that "you b0rked webpack" but really, tough titties to those folks. It should be opt-in.

Btw, digging through the TypeScript source, I see importModuleSpecifierEnding -- can this be (ab)used to cause the emitter to use .js endings?

Maybe mush this proposal with the tsconfig schema? https://github.com/domenic/package-name-maps

At this point I would be happy if TypeScript could only automatically rewrite imports using the paths specified in tsconfig. Even without browser support that would probably solve most of the pain points for me.

Any updates? Any attempts to solve this issue? We have all the major browsers supporting es modules by default, the standard for those require the extension to be there. I understand we can add it manually and tsc will understand that, but this should not be mandatory, why can't it just work?
Sorry for whining but this issue is very old now and nothing has yet been done...

How is this even a thing? The typescript compiler _knows_ that the target output is a JS file. I've been browsing these threads for 15 minutes and I still don't understand why it omits the extension.

There are common use cases, which people use, where the lack of extension allows for a more flexible infrastructure. Node require hook and webpack loader are two such cases.

Then, maybe the Typescript compiler could accept an option to add the extensions or not. Maybe it could even accept a set of regex in order to add (or not) the extension, like the include option (https://www.typescriptlang.org/docs/handbook/tsconfig-json.html).

Yea, it's been proposed at least once before already, why not take that into consideration? TS already has few experimental features that might change in the future once implemented in the browser, but TS already has flags for those unstable specs. Why not just have even an experimental flag addImportsExtensions where it would just do the module += '.js', that's it! No funky logic, no extra features. It could be a different target if you prefer to keep it as a separate target. More than that, if you accept any of the above I will personally dig through tsc code and make a PR for that, just don't want to lose my time if it wont be accepted anyway.

You could simply use a custom transform to rewrite imports in the emitted code (adding '.js' or using the resolved path of module imports).

How to Write a TypeScript Transform
ttypescript: wrapper for tsc that applies transforms during compilation

Maybe there's already an existing transform that does what you need.

I know that is possible, I've played around with compiler API quite a bit (though not as much as I'd want to). The thing is, it adds additional tool to the project and additional tools have additional maintainers which might or might not respond to issues fast enough if at all (often not by their fault, we all have lives and work). Small projects are often abandoned, and doing everything ourselves just moves the responsibility to us so instead of taking care of the business code we spend time on the tooling.
Taking the above into account, a lot of projects will not even consider that as a solution and instead add additional build steps like rollup etc, which adds additional config overhead and get us back to additional maintenance cost.
To sum up: I prefer to spend my time on a PR to the official TS, where both I and the community would benefit from it, than spend it on a custom solution that I could eventually drop (by not having time or many other reasons), or that could not be used by anybody else in the world.

@ajafff It could be a solution, but I think that the main problem is the following one: the Typescript transpiler generates wrong source code in the browsers. It's good to have transformations, but I see that as a workaround. I wonder if I have to care about transpilation details and need to implement my own transform or is something that should be managed by the compiler.

Transformations are a really poweful tool, we could write an entire new transpiler as a transformation, but I think this is not the right way.

Yea, it's been proposed at least once before already, why not take that into consideration? TS already has few experimental features

TypeScript has exactly one experimental flag that was added 3 years ago, and it was for an ECMAScript proposal which is no longer compatible with the current proposal.

the Typescript transpiler generates wrong source code in the browsers

I understand your expectations, but given that the compiler doesn't rewrite the paths that you wrote, I'm surprised you don't consider your own code's imports being "wrong". 😉

I'm sorry @DanielRosenwasser , have you heard of a thing called npm? People post their packages there so we all can use it. I understand you might think very highly of me, but sadly I am not an author of great majority of those so I cannot edit them adding the extensions.
If my projects only used my code, I'd be more thank thankful for what typescript brings, as it's truly my right hand in coding (aside of WebStorm). I am however using 3rd party packages like I think most of us and those not necessarily contain the extensions. Some projects, like rxjs, literally wait with hope that typescript will provide the way to add extensions, otherwise it would require them to change the whole build process, so we are back to spending extra time on tools instead of product.
Now please, could you just answer 3 questions?

  1. Is typescript team willing to ship this feature?
  2. If yes, would you accept a PR?
  3. If no, when are you planning on releasing this?

If the answer for the 1st question is 'no', please, close this issue declaring officially that you are NOT willing to ship this feature, don't make others wait if it's not to come.

Yea, it's been proposed at least once before already, why not take that into consideration? TS already has few experimental features

TypeScript has exactly one experimental flag that was added 3 years ago, and it was for an ECMAScript proposal which is no longer compatible with the current proposal.

the Typescript transpiler generates wrong source code in the browsers

I understand your expectations, but given that the compiler doesn't rewrite the paths that you wrote, I'm surprised you don't consider your own code's imports being "wrong". 😉

It's a good point @DanielRosenwasser , I just considered my own code right because it seems to be right reading the Typescript spec (https://github.com/Microsoft/TypeScript/blob/master/doc/spec.md#11.3).

I'm pretty sure you've read the ECMAScript specs. In the spec it's not determined if a module should end with an extension or not. In the specs, the modules import are resolved using the HostResolveImportedModule algorithm, but the definition is ambiguous. The problem is not the ECMAScript spec. The problem is that browsers resolve modules as if the [ModuleRequest] as defined in the specs was a path to the resources.

Keeping this in mind, just go to the home page of the language: https://www.typescriptlang.org/.

In the footer of the page you can read the following lines:

Starts and ends with JavaScript

TypeScript starts from the same syntax and semantics that millions of JavaScript developers know today. Use existing JavaScript code, incorporate popular JavaScript libraries, and call TypeScript code from JavaScript.

TypeScript compiles to clean, simple JavaScript code which runs on any browser, in Node.js, or in any JavaScript engine that supports ECMAScript 3 (or newer).


You promise a code that runs on every browser, but it doesn't seem to be true, thats why this issue remains for more than a year.

As @Draccoz points, we just want to know what are you doing with this issue. But it's a bit frustrating to read one thing in your home page and the opposite in this issue.

No, we're not willing to ship anything related to this until there's at least clarity around things like ES interop in Node and a reasonable strategy for shipping transitive dependencies you'd use from npm in the first place. If you weren't using TypeScript, you'd have the same problem around dependency management and resolution, so it makes no sense for us to come up with something that the JS ecosystem at large might develop independently. But even if those issues were resolved, I can't make guarantees that we'd make any change here period.

You promise a code that runs on every browser, but it doesn't seem to be true, thats why this issue remains for more than a year.

I think this interpretation is too literal. var fs = require('fs') doesn't work when you run it in the browser, HTMLDivElement is not defined in Node, and String.prototype.startsWith doesn't work on older browsers. To solve this, people have made tools and libraries/polyfills outside of TypeScript because they apply to the broader JavaScript ecosystem.

So can you please close this issue? This is a blocker for other projects that await for TS to do it or declare you are not doing it. If you can't just add a simple flag, close this issue and make others aware of your decisions.

@DanielRosenwasser Thank you for your quick response, I really appreciate that. I think it's a wise choice.

I think this interpretation is too literal. var fs = require('fs') doesn't work when you run it in the browser, HTMLDivElement is not defined in Node, and String.prototype.startsWith doesn't work on older browsers. To solve this, people have made tools and libraries/polyfills outside of TypeScript because they apply to the broader JavaScript ecosystem.

Of course it is, but ¿what else could think anyone that knows nothing of Typescript? The fact that my interpretation is too literal is as true as the fact that this text can "confuse" anyone that knows nothing about Typescript 😉.

Maybe you could update your docs (https://www.typescriptlang.org/docs/handbook/modules.html) in order to reflect the actual behavior.

@DanielRosenwasser thank you again for your response. I have been waiting for something similar for a year.

The reason I wanted this feature was so that I could see what sort of web app I could create without any build tools. Until then I can use that script I wrote earlier.

For people like me looking for a solution to being able to use ES modules and TypeScript in the browser today, I found https://github.com/guybedford/es-module-shims. It acts as a sort-of polyfill for package name maps while we wait for spec finalization and browser implementation. It solves @QuantumInformation's problem of wanting to not use any build tools (my problem as well) when authoring a simple web app in TypeScript (aside from the TS compiler).

import 'knockout'

export class MyViewModel {
    greeting: KnockoutObservable<string>
    target: KnockoutObservable<string>
    constructor() {
        this.greeting = ko.observable('hello')
        this.target = ko.observable('world')
    }
}
<!DOCTYPE html>


md5-f28d4b503a1603c40bfeb342f341bfbe


<main>
    <span data-bind='text: `${greeting()} ${target()}`'></span>
    <script type='module-shim'>
        import 'knockout'
        import { MyViewModel } from 'index'
        ko.applyBindings(new MyViewModel())
    </script>
</main>

In theory, once package name maps are supported by browsers you can just find/replace type='module-shim' with type='module' in your HTML files and change the packagemap script to whatever ends up being finalized for packagemap inclusion in the spec.

It's probably also worth noting that the .js extension isn't mandated by browsers or anything - you can always configure your webserver to be more unpkg-like and serve .js files from extensionless request URLs. It's all configurable on the webserver side.

@weswigham that can be very problematic though, because tsc (and classic node module resolution) will know that ./foo and ./foo.js refer to the same file, but browsers will treat them different, even if the webserver redirects. You'd have to be sure that you refer to a file in exactly the same way at every import.

By the way, this issue doesn't prevent TypeScript generated modules from being used in browsers today. Just always import files with the .js extension. tsc does the right thing and resolves types from the .ts file, and you get browser compatibility.

@weswigham Keep in mind that there are situations where you are serving files without having access to the webserver. GitHub Pages, IPFS, S3, etc.. With the advent of single page app frameworks, it is becoming more and more common to run "serverless" (where serverless here means without a server you control/configure serving your assets), so you can't rely on server side filters to serve apple.js when a request is made for apple.

You'd have to be sure that you refer to a file in exactly the same way at every import.

Can always make one or the other 404 depending on your authoring preference. If you used symlinks within your project they'd cause a similar issue that you'd need to have to choose how to deal with, too.

Just always import files with the .js extension

Ya, that works fine, too.

@QuantumInformation Is your snippet used here is for open use? Can I use it in a project? 😃

@distante yes you can use it

FYI if you are using Jest to test and change to extensions to .js in the source - it breaks
but you can add the following to your jest config to fix that

  "jest": {
    ...
    "moduleNameMapper": {
      "(.*)\\.js": "$1"
    }
  }

Hello @MrAntix,

I think jest is the library that should adapt to TypeScript, not the reverse.

I have another site that I want to not use a build tool with. Is this the best we are at:

https://github.com/microsoft/TypeScript/issues/16577#issuecomment-452312753

As a workaround, you can just do it like this (even though its a ts file, not js) 😢

Screenshot 2019-06-05 at 22 47 49

If anyone wants to try it out:
https://github.com/QuantumInformation/web-gen-bot

However, I can't get source debugging to work with the current tsconfig, will report back.

In the end I went back to webpack, node_modules was the killer when trying to get away from build tools in the browser.

It's not only Jest -- if you use tsc to compile TypeScript before publishing, any JS module importing the resulting package will break because of the differences in module resolution.

The discussion here seems to have forked a bit into two related but ultimately separate issues:

  • Typescript should generate files that can be natively loaded in a browser
  • Typescript should solve the fact that node module paths aren't always valid URLs.

I think we need to focus on the first issue, as the second one is clearly something that typescript cannot solve by itself.

The first issue, however, is absolutely something Typescript can solve and should prioritize, as it's a major blocker for most people wanting to use Typescript to build webapps targeted at modern browsers.

In my opinion, the issue boils down to this:

If running tsc generates one file with a .js extension and then another file that imports the first .js file, there's no ambiguity about what extension to use, and there should be an option to include the extension.

For all import statements pointing to files other than the files Typescript generates, I think it's fine to leave those unchanged (as Typescript does today).

As per the first option, I think a command line arg would be best, and it would be off by default. I say this because, if I use webpack, I don't want .js added to the imports as webpack takes care of module bundling.

Anyway, node_modules was the killer that stopped me using the hack for native module browser apps.

I say this because, if I use webpack, I don't want .js added to the imports as webpack takes care of module bundling.

Webpack handles the .js extension perfectly fine though, so there's no difference.

In fact it's sometimes necessary to use .js with Webpack (in the situations where there are multiple files with the same name but different extensions).

So could you explain more about why you don't want .js imports with Webpack?

When I say I don't want I meant I don't need for my use cases.

I don't see a compelling reason why it shouldn't be the default behavior then (always reluctant to add another config flag...)

Setting the imports to .js causes ts-node to cease to work properly for the file. This is because ts-node only compiles one file at a time and if the emitted file contents ends up with require('./foo.js') then when nodejs processes that file it will try to load ./foo.js which doesn't exist anywhere on disk and thus it will fail. When the code does require(./foo) on the other hand, ts-node's handler will be called at which point it can compile ./foo.ts into JS and return that.

If TypeScript emits .js extensions in all cases, ts-node will run into the same problem. However, if there is an option to toggle whether an extensions is automatically added or not then ts-node can set that compiler option to off, which will allow the current system to continue to work.

Since Node.js --experimental-modules requires mandatory file extensions, the API for this is straightforward without needing dependency analysis - an option like --jsext can rewrite any .ts extension to a .js extension. JSON / other format imports can then work fine with this too, provided the user has already explicitly included all their extensions. The only problematic case is a package name that ends in .ts like import 'npmpkg.ts'. This case is extremely rare, but to comprehensively handle it in the resolution the rule could be to make an exception for _bare specifiers_ along the lines of - if the bare specifier (not a URL, or relative path) is a valid npm package name (matching /^(@[-_\.a-zA-Z\d]+\/)?[-_\.a-zA-Z\d]+$/, from https://github.com/npm/validate-npm-package-name), then ignore the .ts extensions in the rewriting.

I have created a TypeScript compiler transformer that will append a .js file extension onto any relative import path. This means if your .ts file has import { Foo } from './foo' it will emit import { Foo } from './foo.js'. It is available on NPM, and instructions for how to use it can be found in the project readme.

https://github.com/Zoltu/typescript-transformer-append-js-extension

If something like this were to be integrated into TypeScript compiler (as a compiler option) it should probably be smarter about deciding when to append the .js extension and when not to. Right now it looks for any path starting with ./ or ../ and that doesn't have a . anywhere else in the path. This means it will not do the right thing in a number of edge case scenarios, though I question whether anyone _actually_ will run into those edge cases or not.

@MicahZoltu great to see userland solutions to this. I think it is quite important though that the mental model becomes always include file extensions, such that the TypeScript extension option can become _turn .ts extensions into .js extensions on compile_. That avoids the resolution edge cases, leaving just the case of npm package names that happen to end in ".ts", which can be handled as I discussed in my previous comment.

@guybedford Including .js extension in a .ts file makes it so the file cannot execute in ts-node. Solving the problem in ts-node is far from non-trivial due to the way NodeJS does file resolution. This means that if you publish a library with hard coded .js extensions, the library will not work for anyone using ts-node. See discussion on this over at https://github.com/TypeStrong/ts-node/issues/783.

@MicahZoltu I mean including .ts extension in a .ts file rather.

@guybedford Including .js extension in a .ts file makes it so the file cannot execute in ts-node.

This is yet another reason why auto-executing TypeScript in node and browsers is a bad idea.

@MicahZoltu I mean including .ts extension in a .ts file rather.

I think the problem with this is that it breaks the equivalence between a .ts file and a .js/.d.ts pair. This means that if you're importing a file that's compiled with the current project you use .ts, and if you move the file to a different project you need to change imports to it to use .js.

It also means that the output doesn't work in standard environments without the transformer. Given that there's no way to install a transformer from a .tsconfig file, that means you always have to use a custom compiler. That's a pretty big barrier to put in place for the small benefit of using .ts instead of .js.

This means that if you're importing a file that's compiled with the current project you use .ts, and if you move the file to a different project you need to change imports to it to use .js.

External / internal import boundaries are well-defined things. If a dependency moves from being an internal .ts file of the current same-build, to being an external .js file of another package import that has its own separate build or perhaps even isn't TypeScript, then yes, you have to change the extension as it's a completely different concept.

It also means that the output doesn't work in standard environments without the transformer.

While a transformer can help us explore this, a --ts-to-js or similar compiler flag / option is very much needed to solve this issue.

@guybedford You convinced me that putting in .ts was the right thing to do for the plugin. However, upon trying to implement it I learned that TypeScript doesn't actually allow it!

// foo.ts
export function foo() { console.log('foo') }
// bar.ts
import { foo } from './foo.ts' // Error: An import path cannot end with a '.ts' extension. Consider importing './foo' instead
foo()

Hello, What about this?


input:

// src/lib.js.ts
export const result = 42;
// src/index.js.ts
import { result } from "./lib.js";

console.log(result);

output:

// build/lib.js
export const result = 42;
// build/index.js
import { result } from "./lib.js";

console.log(result);

issue #30076

A sole .ts extension on a TypeScript file feels most appropriate to me. Often you don't know until compile time what the target is. For example, in many of my library projects I have multiple tsconfig.json files that target different environments, such as targeting modern browsers which should emit .js files and map import paths to .js, or targeting modern Node, which should emit .mjs files and map import paths to .mjs.

@MicahZoltu that importing files ending in .ts is an error is possibly to our benefit actually. Because this means the .ts to .js extension rewriting on compile can be added whenever .ts extensions are used without needing a flag :)

Perhaps a PR to enable trailing .ts extensions being supported and rewritten to .js extensions (possibly under a flag to enable this feature, but a flag that can be removed in time) would be a great way to start along this path.

//cc @DanielRosenwasser

This issue is really long already so someone might have already said this, so at risk of repeating what has already been said:

TypeScript should allow developers to specify extensions in imports because that it valid JavaScript. And this is not about JS/TS extension only! In ESM, any extension is valid as long as the MIME type is correct.

import actuallyCode from './lookLikeAnImage.png';

…should be valid as long as the server serves valid JavaScript in that file and sets the correct MIME type. This goes further as the same could be true for TS! A TS file could be just a JS source code served with JS MIME type and so a valid path for an ESM module import.

IMO TypeScript should only consider imports without an extension (like it wants you today) and leave the ones with alone, no error, warning etc. Otherwise it rejects valid JavaScript code which is pretty unfortunate for something that claims to be a superset of JS.

Sorry if this is not the right place to raise this, I saw a few other issues: #18971, #16640, #16640 but issues on this topic seems to be getting closed left and right so I assume this is the "main" one since it was allowed to stay open.

Playing with NodeJS v12.7.0

salathiel@salathiel-genese-pc:~/${PATH_TO_PROJECT}$ node --experimental-modules dist/spec/src/ioc
(node:15907) ExperimentalWarning: The ESM module loader is experimental.
internal/modules/esm/default_resolve.js:59
  let url = moduleWrapResolve(specifier, parentURL);
            ^

Error: Cannot find module '${PROJECT_ROOT}/dist/spec/src/ioc' imported from ${PROJECT_ROOT}/
    at Loader.resolve [as _resolve] (internal/modules/esm/default_resolve.js:59:13)
    at Loader.resolve (internal/modules/esm/loader.js:73:33)
    at Loader.getModuleJob (internal/modules/esm/loader.js:149:40)
    at Loader.import (internal/modules/esm/loader.js:133:28)
    at internal/modules/cjs/loader.js:830:27
    at processTicksAndRejections (internal/process/task_queues.js:85:5) {
  code: 'ERR_MODULE_NOT_FOUND'
}
salathiel@salathiel-genese-pc:~/${PATH_TO_PROJECT}$ node --experimental-modules dist/spec/src/ioc.js
(node:16155) ExperimentalWarning: The ESM module loader is experimental.
internal/modules/esm/default_resolve.js:59
  let url = moduleWrapResolve(specifier, parentURL);
            ^

Error: Cannot find module '${PROJECT_ROOT}/dist/spec/src/observe' imported from ${PROJECT_ROOT}/dist/spec/src/ioc.js
    at Loader.resolve [as _resolve] (internal/modules/esm/default_resolve.js:59:13)
    at Loader.resolve (internal/modules/esm/loader.js:73:33)
    at Loader.getModuleJob (internal/modules/esm/loader.js:149:40)
    at ModuleWrap.<anonymous> (internal/modules/esm/module_job.js:43:40)
    at link (internal/modules/esm/module_job.js:42:36) {
  code: 'ERR_MODULE_NOT_FOUND'
}

Now, I'm not sure why this is annoying...

At the moment TypeScript doesn't rewrite paths. It's definitely annoying, but you can currently add the .js extension yourself.

@DanielRosenwasser https://github.com/microsoft/TypeScript/issues/16577#issuecomment-309169829

What about implementing behind an option? Like --rewrite-paths (rewritePaths: true) ?

@viT-1 Here is a workaround for the time being: https://github.com/microsoft/TypeScript/issues/16577#issuecomment-507504210

@MicahZoltu I just solve my usecase by SystemJS bundling (tsconfig outFile option)

I haven't read every comment people have written about this but I don't know understand why I have to write .js a gazillion times. I want the computer to do it for me.

@richardkazuomiller We all do, but @DanielRosenwasser stated in https://github.com/microsoft/TypeScript/issues/16577#issuecomment-448747209 that they are not willing to do that for now (I am surprised this issue is still misleadingly open for such a long time after the above statement). If you want the computer to do that for you, consider using a bundler or some 3rd party tool to handle import paths rewrites.

Who wants me to close this issue?

Please don't close, I'm still waiting to see how will the TypeScript team respond to how are they going to approach ESM imports which allow for any extension to be used and rely on MIME type instead. At the moment this is possible in JavaScript but not in TypeScript. (Please see my previous comment for details.)

@TomasHubelbauer This is why I proposed a flag in config file, it could indicate that all extension-less imports should have .js (or any other configured) extension added. This would be opt-in, so default could be false, making existing projects or projects with different needs work just as is.

As previously said:

Not sure which version of TS added it, but imports such as './file.js' now work (even if the file is actually file.ts).
TypeScript resolves the file fine, and outputs the complete .js import to the target.

This makes sense because any valid JavaScript is valid TypeScript. Since you can do:

import foo from './bar.js'

...in JS, you should be able to do it in TS as well. If you do that, you solve the issue of using native ES6 modules because you've got your extensions correct.


Let's also not forget that when the browser sees an import for ./foo/bar, it actually makes a request for it. The fact that nothing was served was due to the server. You could configure it in a way that requests for /foo/bar are satisfied with /foo/bar.js. Not saying this is a good solution, or even a good idea, but it would technically work.

flag in config file, it could indicate that all extension-less imports should have .js (or any other configured) extension added

This would solve the vast majority of cases. Would TypeScript team be willing to consider a PR for this config option?

NodeJS now _by default_ behaves the same way as browsers when it comes to relative path module resolution. It will no longer infer a .js extension by default. This means that the current behavior of TSC results in emitting JS that is invalid in all runtime contexts. It feels like this issue should now be addressed since there is clarity on both browser and NodeJS behavior for ESM.

https://nodejs.org/api/esm.html#esm_customizing_esm_specifier_resolution_algorithm

The new resolver for esm needs a new resolution mode - moduleResolution: node really is just the cjs resolver for what I can now call "old versions of node". If you intend to target the new esm resolver, we'll need a new resolution mode to more appropriately match. Especially since node still fully supports the old resolver in all existing configurations. (Do note: making such a new resolver _our default_ would be difficult)

Though! While there is _disagreement_ about it and _some people will choose to state opinion as fact_, there is still a --es-module-specifier-resolution=node flag that returns the expected extension behavior and may yet still become the default - as the docs say, despite being unflagged, es modules in node are _still experimental in nature_.

Also, as a reminder for the original request: we don't rewrite imports. Ever. At all. Specifier in === specifier out. Specifiers describe structural intent, and it is not our goal to implicitly map from one intended structure to another. You could also think about it this way: If you wrote const varFoo = "./Foo"; import(varFoo) would we somehow add the extension there, if needed? No - that's ridiculous - to handle all forms of dynamic input we'd need to wrap dynamic import at runtime, and suddenly we've become a module loader of our own, layer atop the builtin platform loader. Instead, we'd provide a resolver mode that appropriately errors when you omit the extension (which, ofc, you can undoubtedly already find lint rules for).

If you find this thread and you _really want_ your input sources not to have extensions, and you _really need_ your output to still work with that when targeting (node es) modules, you should provide feedback over at https://github.com/nodejs/modules/issues/323 with your rationale, so it can work that way in the target platform itself, as that is ultimately what we defer to.

suddenly we've become a module loader of our own

Doesn't tsc have to be a functional module loader no matter what? You can't type-check against an import if you can't find the module it came from. I just want to see all the various module loaders behave in a broadly compatible way, or at least in a predictably-different way that's reasonably painless to accommodate.

NodeJS now by default behaves the same way as browsers when it comes to relative path module resolution. It will no longer infer a .js extension by default. This means that the current behavior of TSC results in emitting JS that is invalid in all runtime contexts.

Isn't this another reason to just use the .js file extensions in specifiers like TypeScript already supports? That solves everything except that it'd be great if tsc errored on extensionless imports.

Doesn't tsc have to be a functional module loader no matter what? You can't type-check against an import if you can't find the module it came from. I just want to see all the various module loaders behave in a broadly compatible way, or at least in a predictably-different way that's reasonably painless to accommodate.

We don't aim to provide a runtime loader implementation - just a compile time analysis that reflects whatever runtime loader you target. We can't possibly remap all imports without a runtime component, which we do not have, nor aim to have.

Doesn't tsc have to be a functional module loader no matter what?

Not at runtime. tsc needs to understand how the runtime of choice will resolve specifiers and implement that at build time, but runtime is purely left up to the environment.

You could also think about it this way: If you wrote const varFoo = "./Foo"; import(varFoo) would we somehow add the extension there, if needed?

@weswigham This is a strong argument regarding dynamic imports, but I don't _think_ you can do that with static imports? I can appreciate the argument that the strategy should be the same for both, but I think it is worth mentioning that static imports fulfill a very different role from dynamic imports and I don't think it is outright _unreasonable_ to have a different strategy for static import rewriting and dynamic import rewriting.

Isn't this another reason to just use the .js file extensions in specifiers like TypeScript already supports? That solves everything except that it'd be great if tsc errored on extensionless imports.

@justinfagnani There are two problems with that:

  1. A runtime that executes TS natively (such as TS-Node) will fail to work if you specify .js extensions on your imports.
  2. You are lying to the compiler (IMO) if you include .js extensions in your imports.

Regarding (2), if I have two TS files a.ts and b.ts and a.ts does import ... from './b.js', I am telling the compiler" I have a sibling file named b.js. This statement is not true. to make matters worse, if you have both .b.ts and b.js (which may not actually be the derivatives of each other), it now becomes ambiguous which one you are referring to _even though you explicitly included an extension_.

I am of the opinion that the user should tell the compiler what they actually want (as a user) and that is to either "import any file with a name of X and any extension" (in the case of import ... from './foo') or "import specifically this file" (in the case of import ... from './foo.ext'). If the compiler detects that you are importing a TS file, and the compiler proceeds to emit a .js file, then I believe the compiler should update the import to correctly import the appropriate file. This may mean changing a .ts to a .js in the import statement.

@justinfagnani There are two problems with that:

  1. A runtime that executes TS natively (such as TS-Node) will fail to work if you specify .js extensions on your imports.

This is a bug in TS-Node. It's not matching the behavior of tsc.

  1. You are lying to the compiler (IMO) if you include .js extensions in your imports.

It's actually the opposite - you're telling the truth about what will actually be imported. You will _not_ be importing .ts file, but a .js file. There also should be no visible difference between a .d.ts/.js pair and a .ts file. They're interchangeable. The only proper way to accomplish that is to import the .js file - what the .ts file will be after compilation.

@justinfagnani

There also should be no visible difference between a .d.ts/.js pair and a .ts file

This is not always true. You can configure your server to serve all of JS, TS and DTS with a JS MIME type and then import them all separately in JavaScript and the runtime will dutifully execute them all as JS. ESM doesn't care about extensions at all.

That's why I think TS users should actually be forced to include a .ts extension and extensionless would be an error unless actually importing an extensionless file or that name. And TS would rewrite the imports to .js in the output. And if a JS file existed (not one generated by TS) that would also be an error.

This is a bug in TS-Node. It's not matching the behavior of tsc.

It is a limitation of the NodeJS loader, not a bug in TS-Node: https://github.com/TypeStrong/ts-node/issues/783#issuecomment-507437929

This is not always true. You can configure your server to serve all of JS, TS and DTS with a JS MIME type and then import them all separately in JavaScript and the runtime will dutifully execute them all as JS. ESM doesn't care about extensions at all.

I'm very well aware of that, but the browser will also not execute TypeScript. It's nearly universally true that the file that the environment actually loads is a JavaScript file.

My point though is that if you have a .js/.d.ts pair already, say from a 3rd party package, and import the .js file, tsc will see the .d.ts file and load the types. ie, a .js/.d.ts pair _acts_ like a .ts file, and this is how it should be, since this is how you can transparently port from JavaScript to TypeScript without changing the imports of the files that are ported.

This is a bug in TS-Node. It's not matching the behavior of tsc.

It is a limitation of the NodeJS loader, not a bug in TS-Node: TypeStrong/ts-node#783 (comment)

It's still a bug that TS-Node is diverging from the tsc behavior. That makes for .ts files that don't work in both tsc and TS-Node, and I would strongly consider tsc as the standard-setter which TS-Node should follow.

My point though is that if you have a .js/.d.ts pair already, say from a 3rd party package, and import the .js file, tsc will see the .d.ts file and load the types. ie, a .js/.d.ts pair acts like a .ts file, and this is how it should be, since this is how you can transparently port from JavaScript to TypeScript without changing the imports of the files that are ported.

If you load an external _package_, then you'll need a runtime package loader of some kind (e.g., named import maps in browser) that does the mapping of named module to entrypoint file. However, I think your point generally stands though, in the case where you have a relative .js/.d.ts pair. I am on the fence whether in that case you should do import ... from './foo.d.ts' or import ... from './foo.js'.

The .d.ts file is essentially a header describing structure that contains no information on what extension it maps from (the assumption being that generally you shouldn't have multiple files with the same name but different extensions in the same place, and we error on build if you do) - even today, the associated runtime "source" could be .js or .jsx (using jsx: preserve). You could not, broadly, replace a .d.ts reference with .js in an import at emit time and assume it works...

It’s important exactly that tsc not be too opinionated as to the wider
build and resolution assumptions.

Given that it is a multi-tool on a complex boundary it would be expected
that configuration could allow it to behave the way users want.

There has been a very deliberate effort not to expose resolution changes as
part of the tsc compilation process - this in itself forces opinionation on
users and causes friction in their workflows.

On Wed, Nov 27, 2019 at 16:48 Wesley Wigham notifications@github.com
wrote:

The .d.ts file is essentially a header describing structure that contains
no information on what extension it maps from (the assumption being that
generally you shouldn't have multiple files with the same name but
different extensions in the same place) - even today, the associated
runtime "source" could be .js or .jsx (using jsx: preserve). You could
not, broadly, replace .d.ts reference with .js in an import at emit time
and assume it works...


You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
https://github.com/microsoft/TypeScript/issues/16577?email_source=notifications&email_token=AAESFSQS2DQ23RR5KN3RTZ3QV3TLXA5CNFSM4DPRQTY2YY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOEFK2TPA#issuecomment-559262140,
or unsubscribe
https://github.com/notifications/unsubscribe-auth/AAESFSUAP2YO23ZFHCOWVQLQV3TLXANCNFSM4DPRQTYQ
.

Not sure how to handle this scenario but had a doubt related to the same thing.

I have an import map like this in my html file:

<script type="importmap-shim">
      {
        "imports": {
          "@root/":"../../../",
         }
      }
</script>

And I have my ts file where I am importing a file like this:

import '@root/components/page-main/page-main.js';

Now, this setup works well in the browser. But, how will I be able to navigate/use it in VSCode? I mean, I want to ctrl+click on the import and navigate to the file, get autocomplete, type def, etc.

Also, considering that tsc does not rewrite import extensions from .ts to .js , is there any other recommended tool/package I can use to do the same at build time? Thanks.

(I am using https://github.com/guybedford/es-module-shims for ES Modules and import maps)

But, how will I be able to navigate/use it in VSCode?

Lookup the paths compiler option.

Also, considering that tsc does not rewrite import extensions from .ts to .js , is there any other recommended tool/package I can use to do the same at build time? Thanks.

Just use .js and it'll work fine, even if you're referring to a .ts file at build time.

@tvvignesh https://github.com/Zoltu/typescript-transformer-append-js-extension/ for rewriting extensionless imports to .js at compile time. I'm not personally a fan of the "just put .js in your TS files" because your code won't run in ts-node if you do that. If you don't need to run your code in ts-node, then using .js will work.

But, how will I be able to navigate/use it in VSCode?

Lookup the paths compiler option.

Also, considering that tsc does not rewrite import extensions from .ts to .js , is there any other recommended tool/package I can use to do the same at build time? Thanks.

Just use .js and it'll work fine, even if you're referring to a .ts file at build time.

Thanks for your quick reply. I had already set the paths option in tsconfig.json but still not able to navigate by ctrl+click.

This is my paths option in tsconfig.json

"paths": {
            "*": ["www/node_modules/*"],
            "@modules/*": ["www/node_modules/*"],
            "@root/*": ["www/*"]
        }

Just use .js and it'll work fine, even if you're referring to a .ts file at build time.

This is quite impractical when there are thousands of imports.

The problem with "just use .js" is that if TypeScript emits .mjs files then your program doesn't work. This means you end up in a situation where you are writing TypeScript that _either_ works in the browser _or_ works in NodeJS. You no longer can write TypeScript that works in both.

I believe the argument here is, "That isn't TypeScript's fault, that is the browser and NodeJS diverging".

Node 13.2 supports .js extensions just fine.

@justinfagnani For ES modules? I thought NodeJS would only load ES modules if they had a .mjs extension, otherwise the files were treated as CommonJS?

I feel this whole thread is a bit misguided. Explicitly writing the file extension would solve this entire problem. If you are importing a JavaScript file, write the .js extension. If you are importing a TypeScript file, write the .ts extension. This is allowed in the standard and the TypeScript compiler understands it all equally well. The only problem is that the compiler states .ts extensions are errors. This doesn't affect a pure transpilation, but is only a type error. TypeScript should fix this, as obviously a .ts file extension is valid, since we are authoring in TypeScript. To get around this issue, Deno.js has a VS Code extension: https://marketplace.visualstudio.com/items?itemName=justjavac.vscode-deno

With the advent of WebAssembly, web applications are going to start importing more and more file types. It will become increasingly important to explicitly state if you are importing a .wasm, .js, .ts, .rs, .c, etc file

Anyone actually used the script I wrote?

https://github.com/microsoft/TypeScript/issues/16577#issuecomment-310426634

any feedback if so?

@QuantumInformation I haven't used your script, but the TypeScript transformer plugin I wrote does basically the same thing. 😊 https://github.com/Zoltu/typescript-transformer-append-js-extension/

First of all, I'm not gonna try again to convince typescript to implement it, I get it, you won't do that.
I am reading the comments though and wondering: all those people saying that adding .js to paths will solve it all - do you really not use 3rd party packages? How is me adding extensions to my imports fixing what's in node_modules?

@MicahZoltu oh nice one

What do you mean? You would explicitly write the file extension of the file you are importing from node_modules, if using a path. But wouldn't that be a bare import usually anyway?

If you specify relative path it won't be bare. I am however talking about internal imports of the package I'm importing, look at rxjs for example.

@Draccoz I believe the point being made is that if someone writes TypeScript like import { ... } from './foo' and then publishes that as an NPM package, a user cannot use that library directly in a browser even if it targets ES modules. I believe Microsoft's argument here is that the package is wrong if it is distributing code like that with an expectation that it works in a browser.

You would write the extension of the file that exists in node_modules. import * as stuff from 'rxjs/path/to/file.js'; if it's a JavaScript file, and import * as stuff from 'rxjs/path/to/file.ts'; if they distributed a TypeScript file. This works right now if I'm not mistaken

@lastmjs Yes, but what if rxjs/path/to/file.ts contains import foo from './bar'? You can't change that, since you can't modify the file, since it's a third-party library.

That's why we should push to use explicit extensions everywhere, but yes I see your point. My tool (https://github.com/lastmjs/zwitterion) does this rewriting as a stop-gap solution for those files that have left extensions out.

Yea, my point was exactly that - just adding extensions won't fix 3rd party libraries we can't modify, so that's no solution.
I do get Microsoft point as well, hence I'm not pushing it any longer (aside of the fact that keeping this issue open gives unnecessary hope which blocks packages maintainers from just adjusting build instead of waiting for TS to solve this), it would be awesome though to only have typescript as a build tool and no other dependencies.

yeah but clearly hope is still good for many people

image

Still, it was stated couple of times through those comments that it will NOT be implemented, so why keep it open?

if someone writes TypeScript like import { ... } from './foo' and then publishes that as an NPM package

Are many people publishing uncompiled TS in NPM packages? I was looking at this for my own internal use. I'm making libraries of common code shared between projects and figured NPM is a reasonable way to package them, and our team has moved to TS exclusively so I'd be happy to avoid having to compile the deps. But it looks like the most common use case is to build the package to JS with separate typings, then point modules or main to the JS and types to the typings.

Is there a convention for publishing uncompiled TS to NPM? If so, is this documented somewhere? I get that this is wandering OT from the original issue but I think it's relevant to the answer, because we need to know if "third party TypeScript" (packages) is a supported use case / goal.

@thw0rted Sorry for not being clear, I meant:

...if someone writes TypeScript like import { ... } from './foo' and then transpiles that to JS using TSC today and publishes that JS as an NPM package...

If it will NOT be implemented, I will leave it to MS to close the issue now.

Ah, I get it now. Yes, when I transpile (with target: ES2018) the emitted JS leaves the extension off of the import statement, but it works for me because the consumer is putting everything through webpack/babel and it must be filling in the missing extension for me. I'm up to speed now. It hadn't even occurred to me that my "package" wouldn't work natively in browser without a trip through webpack first.

Yeah, this is a tough one.

@thw0rted I don't use webpack/babel, therefore resolving .js from the box is problem for me.
Some solution/workaround is configuring web server for default resolving sources.

Sorry if I’m repeating what others have said, but this is something I deal with every day so I’d like to throw in another of my unsolicited two cents.

I know that TypeScript doesn’t have to cater to any particular environment (Node.js, Webpack, etc.) but the fact is that there are a lot environments where the extension is always necessary, unless otherwise specially configured, so I don't think they should be ignored. Imports aren’t behind a flag in Node anymore and the import path has to match the URL pathname on the web. While there is no technical need to have .js a the end of a URL, that is the de facto standard.

If the nice people at Microsoft really wants to have no file extension be the default, I won’t argue about it. You’re probably smarter than me, but I would certainly be happier if instead of inferring whether or not extensions are needed based on other imports in the same file, that could be set project-wide so it would work for Intellisense autocompletes for the first import added to a file!

In the meantime, does anyone know of any VSCode plugins that fix this? Adding import/extensions to eslint is the best I've been able to do to deal with this.

Currently I write import lines without .js extension, and then use sed to add .js extension in output files.
https://github.com/yoursunny/NDNts/blob/9f50fcec245b33c7649fa815bbb3dd404eee160e/mk/build.sh#L12-L14
I have to delete SourceMaps during the production build, because they would no longer match after output files have been modified.

I can't write .js extension in .ts source files because it breaks ts-jest.
While it's possible to run Jest on compiled .js files, it breaks Coveralls.

@yoursunny Yes, sed is variant (I was used it too but prefer replace-in-file because of config which can be configured by importmap file mapping) to replace strings in generated js-files, but it smells not well =) May be adding extensions will be useful feature/option for ttypescript with transform plugin (e.g. typescript-transform-paths by @MicahZoltu).

@richardkazuomiller this part isn't true:

If the nice people at Microsoft really wants to have no file extension be the default

TypeScript only:

  1. Allows for Node-require-style module resolution (they should expand this to allow for Node-import-style resolution, which requires file extensions).
  2. In the same compilation unit, resolves .js files to a .js/.d.ts pair even before they're generated by the compiler.
  3. Doesn't modify the import specifiers _at all, ever_.

So the solution to all this is to simply import the .js files with the .js extension. TypeScript will resolve to the right .ts file and not modify the import specifier, so things will just work in browsers and Node >= 13.2.

@yoursunny did you file a bug against ts-jest? They're diverging from the de facto TypeScript standard here. Also seems like a bug in Coveralls if it can't utilize source maps on the .js files.

did you file a bug against ts-jest? They're diverging from the de facto TypeScript standard here.

No, because I don't fully understand how ts-jest resolution works.

Also seems like a bug in Coveralls if it can't utilize source maps on the .js files.

Coveralls receives an lcov report from my build process, and then displays code coverage using files committed to GitHub.
I do not commit output .js files to GitHub. If I run unit testing on .js files, the lcov report will reference the .js files that do not exist on GitHub.
Therefore, Coveralls are unable to find the source files. It'll display coverage percentage, but cannot show which lines are not covered.

The new era is here!

The newly-released Node.js v13 and all major browsers support native ES Modules out of the box, and it'd be great for TypeScript to support these environments in a simple way.

The simplest solution for the most basic of use cases (a website served through a simple static file server) would be to add a new compiler option like "appendJsExtension": true or similar.

@mjbvz The issue in VS Code's repo that you closed is slightly different: it is about using auto-complete to automatically add import statements; when VS Code does this is adds the import statement without a .js extension, and it is easy to overlook this until a runtime failure.

However if TypeScript adds an option like "appendJsExtension": true or similar, then it won't matter much, and we'd be able to write TS code without .js in the source.

Maybe the VS Code plugin, should have an option to enable/disable auto-addition of .js extensions in auto-completed import statements?

@mjbvz The VS Code / Intellisense issue is related, but should not be closed as a dupe. If this issue is eventually closed as WONTFIX, that would actually increase the importance of the VS Code issue.

I'm seeing one point overlooked and not understanding how solutions where the JS extension is special cased would solve this: for ESM, no extension has a special status, even extensionless files are completely fine as long as the server serves them to the browser with the appropriate text/javascript MIME type. That means that code such as:

import something from './javascript-code.png';
import something2 from './javascript-code2.js';
import something3 from './javascript-code3.ts';

This is all valid as long as the files (regardless of their extensions) contain JavaScript code and are served to the browser with the JavaScript MIME type.

Since TypeScript should be a type-safe superset of JavaScript, I don't see a way around allowing imports such as the ones above, because the code is valid JavaScript and if TypeScript won't accept it, it ceases to be a safe superset of JavaScript, isn't that correct?

So solutions where JS extension is either implied or the only one allowed I don't think can cut this? Moreover, even the TS and DTS extensions IMO cannot be special cased, because while this is normally not the case, they could contain JavaScript code, not TypeScript code and server to the browser as such with no problem at all as far as ESM is concerned, given they are served with the right MIME type.

Am I missing something on this front? Is it possible for TypeScript to not implement support for arbitrary (including missing) extensions in code imports, and is it possible for TypeScript to continue assuming TS extensions in module imports, given that file.ts and file (no extension) would then result in a conflict?

Web and Service Workers are also getting support for ES Module import/export.

I fully support and endorse @trusktr 's idea of implementing their idea as a compiler option. It's not that I want to get rid of bundlers like webpack and rollup. But I do believe a feature like this will remove a lot of hassle and time spent on setting up bundlers, when all you want is to compile a simple Typescript project without jumping through hoops or using awkward workarounds.

Please consider this as an option for the Typescript compiler. <3

Does anyone know of a way where you can run the compiled individual js files in a browser without webpack or --outFile, using any kind of module system?

My problem is this: the fairly large TypeScript project I'm working with takes about 35 seconds to compile with webpack into a bundled js file using ts-loader.
The only way I could optimize this was using transpileOnly which cut the compile time to 20 seconds but that is still slow and I loose type checking.
Getting rid of webpack I could get to about 14 seconds using outFile but that is still too slow for me. Using incremental didn't make any difference.

The only way I could get to a few seconds of compile time was by emitting 1 js file per 1 ts file, and using the incremental flag. In this case I imagine ts detects and compiles only the files that really have changed. The issue is I couldn't get this to run in a browser in any of the module targets I've tried: system, amd, es6.
I'm using paths mapping in tsconfig.json and in the compiled js files the errors I'm getting are mostly related to not being able to resolve those aliases.

Is there any way to achieve this?

@andrewvarga I currently run all my compiled TypeScript directly in the browser without bundling or compiling to a single file. The easiest way to do this is:

  1. Make sure all imports in TypeScript files have the .js extension.
  2. Run a dev server that resolves imports of npm package names for dependencies, if necessary. es-dev-server is the simplest I know of.
  3. Run tsc --watch (make sure module is esnext in your tsconfig)

That's it. If you just write the .js extension for imports in your TypeScript, you don't need other tooling to fix up the files for browsers.

@justinfagnani mate, you forgot the most important piece of information - types must be in jsdoc format, any typescript abstraction over native JavaScript will make the parser fail.

@Draccoz tsc will output JavaScript, not TypeScript.

@andrewvarga The simplest way that I know of to transpile without bundling is my project Zwitterion. It is designed to be a replacement for a static file server, so that you don't have to change the way you develop. You can import and export with explicit file extensions (.js for JavaScript and .ts for TypeScript), even in script elements. It is very performant, does caching and auto-reloading when files change. You will have to rely on your editor for static analysis (type errors) though

@andrewvarga I use https://github.com/Zoltu/typescript-transformer-append-js-extension/ and target native ES modules which load in all modern browsers (except Safari, the new IE).

If your runtime NPM dependencies are pretty limited you can load them with es-modules-shim without any bundling. You can see a react template I created that shows all this working together: https://github.com/Zoltu/react-es2015-template

@MicahZoltu Safari does have ES module support, I know that for a fact, based on caniuse and personal experience: https://caniuse.com/#search=modules

@justinfagnani awww sorry, missed the "compiled TypeScript" part, thought you talking about the jsdoc typescript.

Thank you all for the various tips!

What I found is that by using the hard-source-webpack-plugin npm module for webpack and transpileOnly: true in the ts-loader options cut the build time down to about 4-5 seconds.
This of course will ignore typescript errors, so its only useful for quick iterations of building, trying in the browser, and relying on the IDE for any potential errors but I think its very useful during development.

To get the both TS errors and fast compile I still think the only way would be to run modules in the browser but lack of support for import-maps and js extensions made that difficult.
@justinfagnani @MicahZoltu thank you, I will try those options. In general I prefer to avoid using one more npm module but it seems its inevitable.

I tried to make it work using systemjs too but I got stuck with import maps..

@andrewvarga You can try SystemJs by my light example with importmap.
You can also try my heavy example - systemjs & esm alternative, but esm importmaps are not supported by native, and we should resolve modules manually (gulp-replace), or you can try es-module-shims.

So if I am using tsc as my only compiler, targeting esnext or es2015 as my module type. Is there any way I can use the output directly in my browser?

https://github.com/alshdavid-sandbox/typescript-only-compiler

If I understand the rest of this thread correctly, Typescript should be perfectly happy to compile your code if you change this line to say from "./module.js". Then the compiled index.js will have a valid ES6 import statement and it should all work.

That is unless you import some library that either doesn't know how to ES Import or doesn't care :P.

I created a plugin for Gulp which adds .js to import file paths. (also a bonus ts path resolution).

I used Gulp because I don't know of any other task runner that would allow me to essentially attach a simple post-processing step to the tsc compiler (both watch and normal mode). Webpack and rollup both exclusively bundle, and I want to emit es modules.

It works but it's slow to watch as it seems to rebuild the whole thing.

https://github.com/alshdavid-sandbox/typescript-only-compiler/tree/gulp

This is getting more important as node team decided against extensionless imports
https://github.com/nodejs/modules/issues/444

Add import maps also decided against extensionless imports
https://github.com/WICG/import-maps/issues/194

If we want to use typescript compiled es module in browser or node we need to have extensions added by typescript? (for internal modules) and manually for external modules? @weswigham

@chyzwar again, if you simply write the .js extensions in your TypeScript source, then the compiled output will have the correct extensions and the modules will work in browsers and Node. You don't need additional transformation tools or for tsc to do anything for you.

It is simpler for the developer to do the following: If it's a TypeScript source file, write a .ts extension. If it's a JavaScript source file, write a .js extension. That will also compile and run correctly in the browser (.ts will need an application/javascript MIME type). The extension should match the source file type. tsc and ES modules can handle that (at least ES modules in the browser, hopefully Node.js follows the same)

@justinfagnani tsc is fairly happy with this, and it's fair to say "not our problem" here but there seems to be no consensus here about what is right,

Between tsc, vscode, webpack and ts-node none can really agree well against a standard that is just now becoming available natively (now available browser and node).

Example 1

I want to write a modern javascript package, author it in typescript and strip the types out for distribution as a ecmascript module (retaining import and export statements).

Example 2

I'm building a static website using modules in vscode, by default it will auto import without an extension for each first import and you need to manually adjust this.

In these cases, tsc can handle this, and even export aside descriptor files that support then consuming the es module distribution from typescript with full typings (awesome).

But if I want to run test code against the source, I could use ts-node but here ts-node isn't happy.

Similarly, the fact that tsc, vscode and webpack have evolved to do the following:

  1. vscode auto imports without an extension
  2. tsc will happily rewrite imports to a js file as meaning the ts file
  3. webpack does various magic here for default extensions to search for

Are all things that should be questioned now that four implementations (chrome, firefox, safari, node) all expect that the import path should point to something that can resolve directly to a file.

Adding .js to an import should be a temporary workaround, not a definitive solution. If anything, it feels like a hack. A way to fool the compiler. And trying to outsmart a compiler is a horrible ideology to follow imho. I can only imagine how confusing this must be for people new to Typescript. A language that should help Javascript developers write _better_ code.

As number of people mentioned already, if you add .js to the import, TypeScript will output them and everything works (at least within your code). Reasoning that tsc should add extensions because they do not work in the browser is like asking TypeScript to fix runtime errors caused by developer - ok but why the developer can't fix them by himself/herself?
Don't get me wrong, I'd love tsc to be my only dev dependency, but some things just won't happen and will stay as a dream.

I'd love tsc to be my only dev dependency, but some things just won't happen and will stay as a dream.

I must have seen dozens of near-identical comments spanning several years on #3469, including some from the Typescript team. "It's a type-checker, it's supposed to do one thing well, don't expect it to replace your whole toolchain..." It took 3 years, but they still added build mode, because a good case was made that it was the right direction to go.

Webpack and rollup both exclusively bundle, and I want to emit es modules.

@alshdavid Rollup may work for you if you set the output format to esm and set preserveModules to true: https://rollupjs.org/guide/en/#preservemodules

perhaps "extensionless" imports were an original sin, not compatible with the way browsers would work

now it's time to correct our source code and include the .js extensions: we find when you add .js extensions to your source code today, everything works: the browser is satisfied, typescript is agnostic, and the bundlers don't care

so perhaps extensionless was a flaw in our source code that our old tooling was compensating for

Writing .js extension in imports in typescript source code is complete nonsense. It is working, it preserves the extension in the output, but it will probably fail with "could not resolve file" errors in ESLint rules (or TypeScript) in VSCode or whatever editor you are using. Not to mention when you are testing against that typescript soruce code. Then the testing tool should also know about that bullshit behavior. I'm almost sure it will throw errors that it cannot resolve files.

Basic example

src/index.ts
src/foo.ts
test/index.ts

src/foo.ts

export default (a: number) => a + 100;

src/index.ts

// okay, editor (without ESLint) doesn't report error here
import foo from './foo.js';

export default () => {
  console.log(foo(123));
}

test/index.ts

// errm... ts or js ext?! .ts should be the one that make sense
// and that's how the testing too will expect it to be, otherwise will throw
// but then it will detect some weird `.js` ext in the source files...
// which again won't be able to resolve... complete bullshit. 
import main from '../src/index.ts';
import foo from '../src/foo.ts';

test('some boolshit', () => {
  main();
});

test('about foo', () => {
  foo(20);
});

Sorry for the "bullshit" usage, but that's the truth.

I don't think it's that hard to implement basic option that when the compiler sees extension (.ts) in the imports to convert it to .js (or even optionally allow specifying what the output ext to be). When there is no extension, don't preserve the extensions (semi-current behavior?).

Writing .js extension in imports in typescript source code is complete nonsense. It is working, it preserves the extension in the output, but it will probably fail with "could not resolve file" errors in ESLint rules (or TypeScript) in VSCode or whatever editor you are using.

This just isn't true. ESLint, TypeScript, VS Code, and every other TypeScript-related tool I've used _just work_ with this method. It's not a hack, and has a very reasonable rationale around it: that the file you're importing actually _is_ the .js file, and that your extensions shouldn't change just because an imported file is part of the local project, or pre-compiled as part of a third-party package.

Again, I write all my TypeScript code this way and it solves all the problems that are brought up in this thread. The output works great in browsers and Node. It's not bullshit, it just works, _today_.

No, it's not. At least in the most common case where the output directory is dist or build or lib and etc. Your compiled files aren't in the same directory, they just never exist there (in src/) - neither during testing, nor during coding in the editor.

Having/seeing and importing ./foo.js in the above src/index.ts doesn't make ANY sense to me - unless of course if you really mean to import the js/json file, which is completely okay. But to write ./foo.js when you really mean an another typescript source file (./foo.ts) is... brutal. It's misleading and confusing, and definitely wrong.

I'm almost sure if I run the above test/index.ts with Jest it will fail with error that it cannot find/resolve ./foo.js because there is only ./foo.ts.

If we leave everything aside, why it's allowed to have .js extension but not .ts one (ts report error in the editor)? It's basic, simple and stupid inconsistency. As many other "just because" problems in TypeScript.

I actually lost the logic why this issue is named that way when it's actually possible to end with .js extension. I think the problem is the complete opposite - that it cannot end with .ts (as of 3.7+)

why .ts extension in imports is banned totally?!

And lets clarify. I'm talking about development time and experience. I getting the point why we can preserve the .js extension in the compiled output, and why we can add .js ext to imports in a .ts file without reporting errors, and I'm okay with that.

TypeScript is a superset of JavaScript but it is not JavaScript. JavaScript is a compile target for TypeScript.

With that in mind, despite the knowledge that TS will compile to JS, I would expect TypeScript code to behave in an entirely self contained manor.

Writing a .js when attempting to import a .ts file assumes knowledge of the target compile type and assumes that compile target is always JavaScript.

What if we compile TypeScript to a web assembly binary or run TypeScript code using an alternative runtime such as Deno or ts-node.

Import statements ending with .js make no sense in such contexts.

I'd rather be forced to type .ts out in my import paths or have no specified extension assume a TypeScript source file.

It's sad that typescript invented it's own extension instead of being just a syntax sugar like flow.

@phaux I would never want to work with a project, which mixes source and dist files under the same extension, even if they live in different folders. It's like naming all the files index.js only coz they live in different folders...

This issue (which is bigger than TypeScript) seems to block Visual Studio Code's IntelliSense from working in web browsers and Node -- which requires full (relative) file paths in import specifiers. Without this, the generated import statements it creates only work with transpilers and bundlers, like WebPack and TypeScript itself.

The VSCode team's response was that extensions should be added by the code that provides auto-import suggestions. I suspect that's the TypeScript Language Server (not tsc, which the discussion here has been about).

Can someone who actually knows things confirm (or correct) that VSCode gets auto-import suggestions from the TypeScript Language Server?

the situation is a little complicated and nuanced, so while the ".js" extensions initially go against the grain of many people's elegant intuitions and ideals, it might be unwise and confusing for typescript to deviate much from ratified browser standards and shipped implementations

so perhaps we consider the current paradigm as follows:
typescript imports describe what is imported at runtime – that is, your imports are executing in your "dist" directory, not "src" – like fetch calls and the rest – if we can tolerate this idea, the rest becomes simple and consistent

it seems certainly a bad idea to have typescript blindly attaching ".js" to extensionless imports like node does for commonjs (it's worth noting that node esm support requires ".js" the same way as typescript today) – blindly attaching ".js" would make new problems, like importing truly extensionless javascript files, or even other file types like css or json – it would make extensions meaningful and require special parsing logic, creating a stark difference between browser and typescript semantics – and we're obviously not going to rewrite fetch calls the same way, so maybe import should remain similar?

however, i do wonder – is it relatively harmless for typescript to convert ".ts" extensions to ".js"?

of course, that would make it impossible import a javascript file which had a ".ts" extension – but maybe that's one bit of funky magic we could tolerate? on the other hand, it is a funky magical idiosyncratic difference between browser and typescript semantics which developers would have to learn and understand (surely creating weird gotchya's) – so my instinct is to leave things where they stand today

on another hand, if we keep things as-is, the magic is simply relocated to typescript, which has to relate the ".js" imports to the corresponding ".ts" source files – this seems to me like a more reasonable location to house the magic, being internal to typescript instead of trampling browser semantics

  🥃 chase

@rconnamacher, @TomasHubelbauer

This issue (which is bigger than TypeScript) seems to block Visual Studio Code's IntelliSense from working in web browsers and Node -- which requires full (relative) file paths in import specifiers. Without this, the generated import statements it creates only work with transpilers and bundlers, like WebPack and TypeScript itself.

i think there's just some confusion here – it's very possible today to create a typescript library or app which works great with vscode and intellisense, which works in browsers and node, and works in esm or commonjs – a truly universal solution is possible today

i've been working on a handful of open libraries that exhibit this – renraku, cynic, redcrypto, authoritarian

shoot me an email and i'd be happy to explain more

  :wave: chase

Another point is that it results in confusion in what file you're accessing. There is already ambiguity of what tsc looks at after module resolution when you have a .js and .ts file side by side.

/folder
  a.ts
  a.js
  index.ts

If in index.ts, when using moduleResolution: 'node'

// points to a.ts
import * as a from './a` 

// points to a.ts
import * as a from './a.js` 

// compiler emits error
import * as a from './a.ts` 

There is already ambiguity of what tsc looks at after module resolution when you have a .js and .ts file side by side.

OK, sure, sort of, but if a.js is anything but the transpiled version of a.ts then something has gone catastrophically wrong and it's not TypeScript's fault.

@thw0rted if you're compiling to a single JS bundle file, TypeScript will not generate a JS counterparts for the input TypeScript files, only the single bundle file. In that case, it is completely legal to have a JS file for and a TS file of the same name which don't correspond in any way.

It's not "illegal" but that doesn't mean it's a good idea. Do you a have a concrete example of where you'd actually want this?

It would simply be nice to use TypeScript without bundlers, loaders and in a way which emits JavaScript which can be used directly by modern browsers.

As an example:
https://github.com/alshdavid/tsc-website

But I do feel the pain of the problem. The TypeScript team do not want to implement module resolution. It's the same reason the TypeScript compiler can't convert paths to their actual relative paths at compile time.

Converting an import from .ts to .js, or adding a .js to an import without an extension means the TypeScript team would be implementing module resolution and that is out of scope.

A solution for this is for the TypeScript team to provide a way to introduce extensions so we can implement these things.

A solution for this is for the TypeScript team to provide a way to introduce extensions so we can implement these things.

They do, it just isn't exposed via tsc (only via programmatic invocation of the compiler). Luckily, someone built a wrapper around the core compiler that functions _exactly_ like tsc except it supports extensions via configuration. Using this tsc wrapper, you can use an extension like https://github.com/Zoltu/typescript-transformer-append-js-extension/ to auto-add the .js extension.

@alshdavid your example in GitHub does work in browsers without a bundler if you just include the .js extension on imports. I filed a PR to fix it.

@alshdavid May be you interesting in my sample project (esm for modern & IE11 with SystemJS ).
But I'm forced to resolve esm modules manually =(

I have no problem in principle with putting .js in all my imports, but unfortunately it exposes a poor interaction with some tools. For instance, ts-node chokes: https://github.com/TypeStrong/ts-node/issues/783. There doesn't seem to be clear agreement on whose responsibility this is - TypeScript to emit the .js extension (under some circumstances) or every tool that consumes TypeScript sources to do the translation. And as long as everyone is passing the buck, users are suffering with poorly-documented plugins or awkward service workers to work around the interop incompatibilities.

Are there any updates on this? I honestly can't believe that it does not seem to be possible to just use TypeScript normally today without an external module loader or bundler like Webpack. This seems so weird and also frustrating to me. If I have to use some external module to make TypeScript work fully, then that should be started.

I am trying to use this in my Electron app which is why I don't see a reason for using a web bundler and I also would like to not install an external module loader if it is not needed. But this is making me mad. Not even being able to get a basic TypeScript + Electron setup going although both are praised everywhere as being the best solution out there. That's crazy, to be honest. I don't know if I am just missing something but not being able to find an adequate solution is more than frustrating.

And if it's really not me, I don't understand why this issue has not been fixed in 3 years...

I recently completed typescript tutorial and then when I tried to make some ts files I came around this issue. I am completely new to this js-ts world so pardon me if anything is wrong or misleading/misinformed.

what I did was, while importing

I just pust .js extension in the ts file and then when I checked the intellisense it worked also when I transpiled it using tsc it added .js extension as well to the generated output file.

Following is what I did.
tsc -p tsconfig.json

{
"compilerOptions": {
//"module": "amd",
"module": "es6",
"target":"es6",
"noImplicitAny": false,
"removeComments": true,
"preserveConstEnums": false,
//"outFile": "js",
"outDir":"js",
"sourceMap": false
},
"include": [
"testscript.ts"
]
}

Although it worked for me.

My doubt is that since there is not svgwrapper.js to import from
why/ how did it worked? (I'm assuming it doesn't consider extension)

I'm attaching the screenshot for reference.

ts-js-ext-issue

@yogeshjog this is exactly how tsc is and is supposed to work. You import the module with the filename that will be generated. Whether the imported module is originally written as a .ts file or .d.ts/.js pair shouldn't be observable from the importing module.

@yogeshjog this is exactly how tsc is and is supposed to work. You import the module with the filename that will be generated. Whether the imported module is originally written as a .ts file or .d.ts/.js pair shouldn't be observable from the importing module.

Thanks! @justinfagnani for clarification 👍

You import the module with the filename that will be generated.

But TypeScript also allows to omit the extension, which is not allowed according to ECMAScript. It even omits the extension when using auto-import feature in VSCode which is the most annoying thing. You write some JS and TS says it's okay and then it crashes in the runtime.

But TypeScript also allows to omit the extension

When using node resolution. tsconfig is at least setup to allow for other resolution modes that would warn about this, but my understanding is that the team has been letting native module support settle before adding more modes.

which is not allowed according to ECMAScript.

ECMAScript doesn't say anything about import specifiers. That's left up to host environments like HTML and Node. TypeScript supports node resolution when loading modules, and interprets the resolution against the compiler _output_, not the compiler _input_. This way the specifiers work after compilation and they work regardless of whether your trying to import a compiled module or not.

Because TypeScript uses Node resolution which will do a path search to turn './foo' info './foo.js', './foo' is valid. But Node resolution will not turn './foo.ts' into './foo.js', so './foo.ts' is invalid.

But that's within Node resolution. If you're trying to use another environment like HTML or Node's native module support, then TypeScript and the host will disagree about what's valid.

Hopefully native module support is settled enough now for TypeScript to add another resolution setting.

@leontepe You don't need a bundler/loader. You can either use https://github.com/Zoltu/typescript-transformer-append-js-extension/ (my preferred solution) or you can append .js to all of your import statements (beware: this will make your code fail in ts-node).

@leontepe You don't need a bundler/loader. You can either use https://github.com/Zoltu/typescript-transformer-append-js-extension/ (my preferred solution) or you can append .js to all of your import statements (beware: this will make your code fail in ts-node).

@MicahZoltu Well, thank you, but still kinda frustrating that it doesn't just work 'out of the box'. As @phaux said, VSCode suggests its users to make import statements without the extension and that simply does not work in runtime. I seriously question the competence of the TypeScript team here or I got something wrong this whole time and am just plain stupid. But who knows...

Edit: seriously considering just dropping TypeScript for JavaScript + Babel at this point.

I haven't written an Electron application, but is there any possibility of making it do Node-style resolution, where there's an algorithm for trying to resolve imports that tries several things in a fixed order? Seems like it should be OK given that all the sources are being loaded from local resources...

Maybe it's time to add new module resolution to TS? Node.js have native ESM support now, but without explicit extensions in relative paths it forces to use --experimental-specifier-resolution=node flag, which kinda annoying. There is also Deno and browsers. Ideally there should be something like that:
tsconfig.json:

{
    "compilerOptions": {
        "moduleResolution": "explicit",
        "strict": true
    }
}

then Typescript:

import foo from './foo.ts'; // no ts(2691) here

compiles to Javascript:

import foo from './foo.js';

"strict": true means a lot of work!
2020,still not fix, ridiculous.

Just chiming in to say that this issue is blocking our team as well 😢

We'd really like to be able to have a single source that builds to native modules and is also consumable in node and with webpack. Webpack seems to have an issue if you manually put .js into your imports in your TS code. However, you can sort of get webpack to work if you put the .ts file extension in your imports, but of course typescript complains. Not having a file extension results in not having native modules. So, no combination seems to satisfy the browser and the bundlers. If we could write .ts in our imports and have that become .js in the output, problem would be solved I believe (and you'd also get deno compat). Something like @evg656e suggests seems like it should work.

@EisenbergEffect is there a bug filed against WebPack for this? Really sounds like a WebPack issue to be diverging from normal tsc.

@justinfagnani I'm not sure if the problem lies in webpak, ts-loader, or elsewhere. I did spend a lot of time trying to get my optimal build setup in place and didn't come across any solution that checked all the boxes. There are probably ways to do it with custom transformers or post-build steps but I'm reluctant to adopt non-standard setups as it may affect downstream developers negatively.

@EisenbergEffect if WebPack's TypeScript loader doesn't let you import TypeScript files via a .js extension like tsc does, then it's a bug. Like you said, the behavior prevents having the same source build with WebPack and tsc.

Maybe it's time to add new module resolution to TS? Node.js have native ESM support now, but without explicit extensions in relative paths it forces to use --experimental-specifier-resolution=node flag, which kinda annoying. There is also Deno and browsers. Ideally there should be something like that:
tsconfig.json:

{
    "compilerOptions": {
        "moduleResolution": "explicit",
        "strict": true
    }
}

then Typescript:

import foo from './foo.ts'; // no ts(2691) here

compiles to Javascript:

import foo from './foo.js';

People (for instance, me) would love to let one piece of code works among Node.js, Deno, and the Web at the same time. I think this would be a great milestone for TypeScript/JavaScript programming.

If you're using vscode and need an interim solution, adding this to settings.json seems to fix the issues:

"typescript.preferences.importModuleSpecifierEnding": "js"

It will append .js to your import paths (which will resolve the *.ts files in src without error, but retain the .js when transpiling). It's useful when using tsc without a bundler.

While we wait for a tsc solution a low-tech fix to this is to

  1. Copy all source files to a temporary folder
  2. Remove the extension for imports and exports before build

I wanted to share this one-liner in case others can use it. It copies the src/ folder to tmp/ and changes the files there.

npx shx cp -r ./src/ ./tmp/ && npx rexreplace "(^§s*?(?:import|export).*?from§s+?(['\"]).*?)§.ts§2" €1€2 './tmp/**/*.{ts,js,tsx,jsx}'

As part of a build script in package.json (after doing a yarn add --dev shx rexreplace) it could look like this

"scripts":{
  "build": "yarn build-esm && yarn build-tsc",
  "buil-esm": "...Whatever you normally do...",
  "build-tsc": "shx mkdir -p tmp && shx cp -r ./src/* ./tmp && rexreplace \"(^§s*?(?:import|export).*?from§s+?(['\\\"]).*?)§.ts§2\" €1€2 './tmp/**/*.{ts,js,tsx,jsx}' && tsc src/index.ts && shx rm -r ./tmp"
}

I currently keep .js off the import paths in TypeScript sources, to keep Jest happy.
Then, I have a post processing step to add .js.
Source maps are not supported with this method.

tsc -b tsconfig-solution.json -w --listEmittedFiles \
  | node mk/build-post.js

The build-post.js post processing script does the following.

Append .js to import and export of relative paths

For example,

export { X } from "./first";
import { Y } from "./second";

becomes

export { X } from "./first.js";
import { Y } from "./second.js";

Note that I do not use index.ts anywhere, but instead use mod.ts following Deno convention. Thus, I don't need to consider the case of appending /index.js.

Change import to require for CommonJS packages

My code runs on Node ^12.17 and ^14.1, in modules mode. I only publish ES Modules.
However, many dependencies still have CommonJS in their "main" field.
Thus, I should change these to CommonJS, except NodeJS builtin modules.

For example,

import { Server as WsServer } from "ws";

becomes

import { createRequire } from "module";
const require = createRequire(import.meta.url);
const { __importDefault } = require("tslib");

const { Server: WsServer } = require("ws");

But then, webpack is unhappy with these requires, so I have to use:

/// #if false
import { createRequire } from "module";
const require = createRequire(import.meta.url);
const { __importDefault } = require("tslib");
/// #endif

/// #if false
const { Server: WsServer } = require("ws");
/*
/// #else
import { Server as WsServer } from "ws";
/// #endif
/// #if false
*/
/// #endif

In webpack, any package importing my project must use ifdef-loader, and then webpack would see the original import line.

We're also getting hit by this and I can't understand how this is not fixed 3 years after being reported.
We're using WebStorm so the VSCode specific setting won't work.
Using a separate script to fix the output is ridiculous.
This should work without having to use third-party tools.

+1

This is my workaround using a asp.net core 3.1 website (probably works on lower versions)
In the startup.cs:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseRouting();

            var rewriteOptions = new RewriteOptions();
            rewriteOptions.AddRewrite(@"^js/(.+)", "js/$1.js", skipRemainingRules: true);

            app.UseRewriter(rewriteOptions);

            app.UseStaticFiles();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapGet("/", async context =>
                {
                    await context.Response.WriteAsync("Hello World!");
                });
            });
        }

The rewriting middleware will add the extension ".js" to any request targeting the folder /js.
Warning: order of middleware matters here, the UseStaticFiles must placed after the UseRewriter or it won't work.

As everybody here, I would prefer a solution out of the box but until then...

Maybe it's time to add new module resolution to TS? Node.js have native ESM support now, but without explicit extensions in relative paths it forces to use --experimental-specifier-resolution=node flag, which kinda annoying. There is also Deno and browsers. Ideally there should be something like that:
tsconfig.json:

{
    "compilerOptions": {
        "moduleResolution": "explicit",
        "strict": true
    }
}

then Typescript:

import foo from './foo.ts'; // no ts(2691) here

compiles to Javascript:

import foo from './foo.js';

I set up the node/file-extension-in-import rule in eslint-plugin-node to linting config with the idea of manually adding the extension to import statements(as it is better that way) and the "moduleResolution": "node" in TypeScript config and all I can see is ts(2691).

Hopefully what @evg656e said might be worth implementing.

I cant believe this is still an issue.

when you use
"baseUrl": ".", "paths": { "/*": ["./*"] },

you can do an absoulte module import like so:
`import ws from "/hey/connection";

but when adding the ".js" extension, all of the sudden the compiler no longer finds the connection.ts declaration and says:

Could not find a declaration file for module '/hey/connection'. '/home/tobi/Documents/JITcom/Code/Libs/Test_Browser/hey/connection.js' implicitly has an 'any' type.

using a relative path it all works fine.

Please fix this

I am also wanting a fix for this.

Thank you.

It is ridiculous that this is still an issue! :00

I've finally decided to make the switch to TypeScript and this was one of the first issues I ran into. It breaks module resolution in Node's native implementation, and I've only been able to get around it either by using the esm package which seems to be more flexible in accepting names without extensions, or (weirdly) using .js extensions in the import paths inside my .ts files even when the import is referring to a .ts or .tsx file. I'm not sure why the TypeScript compiler accepts that, but at least the build output then includes the .js extension. Terribly hacky solution though.

I'm not even using typescript much on the clientside these days, didn't affect my code quality too much and made things way simpler. Probably JavaScript will have optional types of one point and this issue won't matter anymore.

I leave off the extension on import lines, to make Jest and Webpack happy.
When I compile, I invoke tsc --listEmittedFiles and pipe the output to a post-processing script. That script adds the .js extensions, to make Node (in module mode) happy.
https://github.com/yoursunny/NDNts/blob/fa6b2eb68a9f32a6a2e24e5475275f803236b8f8/mk/build-post.js

@kj

(weirdly) using .js extensions in the import paths inside my .ts files even when the import is referring to a .ts or .tsx file. I'm not sure why the TypeScript compiler accepts that, but at least the build output then includes the .js extension. Terribly hacky solution though.

It's not weird and definitely not hacky, and works perfectly with browser and Node native module resolution.

The resource you're importing _is_ the .js file. The type could either be in a .ts file or in a .d.ts file. A .d.ts file doesn't even need to be a sibling to the .js module, and there doesn't need to be a 1-to-1 relationship between .d.ts files and .js files.

Importing the .js module is the most reliable and stable option that tsc could have chosen. Otherwise there would be cases where you import the types and tsc doesn't know the correct way to rewrite them into an actual module path.

@justinfagnani Ah, I think that makes sense. I didn't mean to imply that anything that TypeScript was doing here was hacky (I'm not in any position to make a claim like that), just that I felt that what I was doing might not be typical. I guess I was thinking about it the wrong way though. It's not TypeScript that imports the module, it's whatever evaluates the build output (or am I misunderstanding)? In that case I can see why it would work this way.

The resource you're importing _is_ the .js file

You're referring to the built module here right? Not a .js file in the source directory, but the built version of the .ts module?

Could it not be that if you explicitly specify the extension .ts or .tsx in a module path, then the output will substitute .js?

I think there may be a misunderstanding of whats going on @kj. When you specify a .ts extension without having compiled your code, its referencing the ts file. But when you compile using tsc it converts the ts file's contents to a js file that is executable in the environment you're in (node or browser).

I understand that @mkay581, but say I have this line in a foo.ts file:

import { Component } from './Component.js';

The 'Component.js' I'm referring to is in fact 'Component.tsx' on the filesystem (there is no .js file, unless TypeScript is understanding this to be referring to the eventual transpiled version of the file?), yet TypeScript accepts this just fine and the same import line is then present in the output (with the .js extension which then works with Node or the browser).

The traditional TypeScript way (AFAIK) would be to specify that import line without the extension, as:

import { Component } from './Component';

That obviously compiles fine too, except the import line in the transpiled .js file doesn't include the .js extension on './Component', which is required by certain environments (and possibly it sounds like the spec, although I haven't read it to be sure).

On the other hand, if I specify the line with the actual filename of the source file:

import { Component } from './Component.tsx';

Then TypeScript doesn't recognise the path and suggests that I should try the import line without the file extension (if I recall, I'm not at my computer right now).

So, the first example feels a bit strange to me, as I'm writing TypeScript I'd expect to be able to import with the .ts or .tsx extension, yet it only accepts the path with no extension (which seems to be at odds with other implementations of the ES module system) or with the .js extension which could only refer to the output file as I have no such source file.

@justinfagnani

The resource you're importing _is_ the .js file.

I don't know how tsc works but that doesn't sound right to me. When you write your code, there is no .js file yet. It's the compiler's job to create it.

TS acts as a layer of abstraction on top of JS and the .js files are the output of the compilation step. Counting on them being there with that specific extension seems to break the abstraction, IMO.

To put it another way, when you're using C or C++ you don't #include the .o files in your code, you include the .h or at most the .c or .cpp.

I'm willing to accept a reasonable explanation for this behaviour in Typescript but unless I'm missing something, this one doesn't seem to be it.

@kj

It's not weird and definitely not hacky

Hard disagree. To me it's the very definition of weird and hacky. 😛 What you're doing here is referencing a hypothetical file. A file that's not guaranteed to exist. (E.g. when bundling code, using AssemblyScript, or deno) You're assuming the output format. But when writing agnostic code (e.g. third-party modules), that's a _dangerous_ thing to assume.

@borfast @frzi Yes that's basically how I felt about it, and I'm glad to see I'm not the only one that doesn't feel comfortable with this. I'm just trying not to make too many assumptions being so new to TypeScript!

@frzi

What you're doing here is referencing a hypothetical file. A file that's not guaranteed to exist

Not hypothetical, tsc knows it'll generate it. It is guaranteed to exist, when using tsc.

Another way to look at it is that if you import the .ts file you're importing a file that won't be there after compilation.

when bundling code

Bundlers run after tsc

@kj mentioned .tsx files, and they drive my point home even more. .tsx only exists to signal to the compiler of that module, _locally_, that the module contains JSX. Importers of the module have no need to know that there was JSX in the file, and they can't tell after compilation. Files can be seamlessly ported between .tsx, .ts, and .js/.d.ts pairs.

Let say you're import a .ts file:

import {Component} from './component.ts';

And you then want to use JSX for the component, so you rename it to component.tsx. Should all the imports fail? Say you migrate from .ts to .js/.d.ts, should the imports fail again?

@borfast

TS acts as a layer of abstraction on top of JS and the .js files are the output of the compilation step. Counting on them being there with that specific extension seems to break the abstraction, IMO.

The abstraction isn't as complete as you imply here. You can import .js files from inside or outside of your TS project. You can add types to a .js file with .d.ts file that lives anywhere in your project. The .js files are what's real after compilation. Everything else just aids the compiler.

@justinfagnani I'm getting a bit lost with what you're saying, but why can't TypeScript just assume that if you give it an import path with .ts or .tsx that it will generate a .js file for that module and therefore should substitute the latter extension in the output?

@justinfagnani

Not hypothetical, tsc _knows_ it'll generate it. It is guaranteed to exist, when using tsc.

But there’s a symmetry implied here that you’re not admitting to.

This “guarantee” you mention holds both ways. If, as you say, “tsc _knows_ it’ll generate [js files]”, then it’s an equally valid question to ask: “Well, then why is tsc NOT acting on this guarantee and apply the .js extensions which it _knows_ is missing from the compiled js?”

Interpreting the fact that “tsc guarantees js files” goes both ways.

So I agree, @justinfagnani, your interpretation _is_ valid, from a certain point of view. But the opposite interpretation is that “tsc should therefore _act on its knowledge_ and perform this process for the developer”. And what bothers me about this entire debate from the beginning is how _negatively_ this interpretation is received, almost to the point of being ridiculed. It’s unreasonable.

In the end, this debate is about _an aesthetic decision, not a logical one_. And my hope is that an ampliative and accommodating tone of voice — as is proper to aesthetic _opinion_ (not logical deduction) — is brought to bear on this issue by its detractors.

In short, please admit the OP’s question is a perfectly valid interpretation — perhaps along with your own. No need for harsh debate.

Thank you

@justinfagnani

Another way to look at it is that if you import the .ts file you're importing a file that won't be there after compilation.

Of course it won't, just like a .h file is not distributed with the executable of a C program. That's exactly the point: you tell a compiler to include source files, not compiled files.

Ok I get too spammed with this conversation to ignore it further.
import statement is NOT part of typescript, it's part of JavaScript environment that executed it. Now, the key word here is JavaScript environment, as THIS is what resolves the paths. Show me a single app that in the runtime imports and executes .ts files (sure, there may be some, but they are what I would call hacky).
TypeScript allows adding .js extensions because it allows native JavaScript inside of it's source files. neither of browser of node.js will execute .ts file, even ts-node transpiles .ts files on the fly, just keeps the result in the memory instead of writing them down to the hard drive (not hacky at all right?).

Now, the proper tsc fix would be:

  • we have .ts file
  • we reference it in another file
  • tsc transpiles that .ts file into .js file
  • tsc then rewrites all direct references to .ts file with .js file (it knows exactly how it transpiled so can update it correctly)

Problem: "modern web dev" is not real web dev and uses node.js way of importing, skipping the extension (and whole file path). tsc now no longer knows what's the output going to be, coz importing my-awesome-module/test can be anything, starting from a file test.js in node-modules/my-awesome-module, through index.js file inside test folder in node-modules/my-awesome-module, ending on some local rewrites with non-js files like ./local-mocks/my-awesome-module/mock.json.

This is where the problem arises: how can a tsc know whats the funky webpack/rollup/super-awesome-new-and-shiny-bundler config for your particular project?

Again: I am all for tsc to rewrite import paths, but it would only work for simple projects, and making it work for only simple projects (which nowadays is a minority as even simple presentation pages use react with overengineered webpack config) is not worth spending time on, if it can be done with a simple custom written script.

Again: I am all for tsc to rewrite import paths, but it would only work for simple projects, and making it work for only simple projects (which nowadays is a minority as even simple presentation pages use react with overengineered webpack config) is not worth spending time on, if it can be done with a simple custom written script.

How would you do it in a simple project without webpack, just with tsc I mean?

I'm reading everyone's comments and it seems to be a fundamental disagreement of expectations of typescript usage here.

When .ts source files are referencing .js files, the problem has already begun.

Typescript was built to ~run~ compile source .ts files -- not .js files. If a dev wants to use TypeScript on their project, they should convert their _entire_ project to typescript and not leave orphaned .js files and try to reference them. And if you don't have the time to change the contents in js files to Typescript syntax, just rename the js files to ts files (or use TS's path mapping to map imports to .js files). Problem solved. :man_shrugging:

EDIT: Corrected "run" to "compile", which is what I meant but I can see how that may have been interpreted differently.

@mkay581 TypeScript was never intended to run anything, just to output JS files.

@valeriob Simple project would probably have like few files at most, which does not need to be bundled. Browsers nowadays have imports built in, no need to get around that. Then it would be as simple as listening to navigation events in the browser, then map each event to matching route, each route could import data with fetch and once returned could be rendrered with non-compiled templating engines (lit-html, HyperHTML, or older ones like Mustache, Handlebars, Pug etc). Done, no need to fancy webpack, framework or any utility libraries, just even listeners, regexp, fetch, promises and a simple js templating engine.

Thanks @Draccoz, than can you tell me how to make this simple scenario working ? https://github.com/valeriob/Typescript_Non_SPA
It's a simple .ts file referencing a JS library (rxjs as an example) and I want to consume it as module in an html page.

@valeriob This is not as trivial as it should be. Most of 3rd party libraries even when they declare they are ES Modules compatible, they are not in browsers world.
RxJS is something I was mostly interested in when bringing it to native flow, but they wait for this ticket to be resolved before they decide to implement it themselves (funny...).
In the architecture I was designing I initially used sed to fix all imports, but then changed my mind to use a simple dev server with path rewriting. My concept is to have no external dependencies in my apps aside of rxjs, typescript and lit-html (node_modules with 4 folders and ULTRA fast CI tests/builds). If TS rewrote paths, it would eliminate the need for my server, though it's around 90 lines of code anyway. It's open source, if anybody wants a link just ask or check organizations in my profile.

As to simple pages, I was refering to those that don't really need any library, like hitting a url -> fetching data -> displaying it -> repeat.

Not needing using any library in Js is pure fantasy since Js has no base library.

The simple scenario is the one I linked, and it's not working. Also it's the most common scenario for ppl that do not want to dive in the crazy webpack/compilation world that complicates things with no real benefit, limit your options very much, make devs fight the tools more than writing your app, and slow your dev loop.

Many mental gymnastics going on here to deny that this issue exists and that it is an important concern for many developers’ productivity.

Explaining that it _should not be an issue_ does not _solve the issue_. Software should conform to the users’ mental models, not impose its own implementation model onto users. (UX stuff)

If users are conceptualizing tsc as a C-like compiler, as per @borfast, then that is the privileged mental mode that needs to be accommodated, despite what tsc’s implementation details are.

The OP’s question has 200+ thumbs up, more than all other issues on the repo.

This issue deserves a community vote. Please consider starting a poll.

If most users want tsc to rewrite their imports, then that is the right answer. At least that is the UX way of looking at things.

It's even simpler than that, if typescript as a language it's a superset of JavaScript, the output of tsc (even the not single output file switch) should be digestible by JavaScript itself.

@weoreference if you define in clear way (not write code, just design) how EXACTLY should it rewrite paths, considering multiple environments (node.js, browser), multiple bundler configurations possibilities (investigating all the current, planned and possible future features of webpack, rollup and parcel and any other bundlers) then sure, then I believe TypeScript team would be full on your side.

If you are not willing to do so or think it's too hard, stop asking them to create a feature nobody can actually describe, it's like "I don't care how, but make this cow fly"...

@Draccoz, you want your car to shift gears so you can drive it faster than 10 Km/h but I'm sure you were never asked by the manufacturer to learn how a gear train works, let alone design one.

As for the issue of bundlers and whatnot, why is that a blocker for this?

Look, I just want to use ES modules, I don't want to deal with all the Babel and Webpack insanity. Why can't this be an optional compiler option that simply changes the file extension to .js? If this behavior conflicts with other configuration options set by the user, it should be automatically disabled, or a warning/error should be shown and the compilation stopped when tsc is executed, so that the user knows they need to change the configuration.

I really don't see why this is impossible.

@Draccoz Hello :) Well, as I see it, you are asking two things:

  1. How it can be implemented

  2. How I, the developer, can signal to tsc that I want the imports renamed in a certain way, for my particular environment

Here are my thoughts.

  1. Please see @borfast ’s reply; it’s a “higher-order” answer, but succinct :)

  2. Maybe I can pass a flag to tsc/tsconfig, such as “renameImports”:true, or some other signal that gets picked up by tsc. If more precision is needed, then maybe the flag shouldn’t be Boolean but rather take a string, perhaps something like this:

standardImportExtension: ‘js’

so all imports without file extensions default to .js

Conclusion:
This functionality only seems hard to implement because you assume that Q2 (import rename scheme) needs to be known in advance by Q1 (i.e. by tsc). But actually, no. I, the human developer, can easily supply this “world knowledge” to tsc without it needing to know how webpack/browsers/ etc work. All this with a simple flag.

@borfast Transmission wasn't asked by a user, it was provided with a car by a manufacturer, user just agreed it's a new feature and started using it - sorry, this isn't a proper argument. Compared to user - car manufacturer example of yours it's more like "I want my car to be a full electric car with 100k miles range and top speed reaching speed of a jet. I don't care how to achieve it, I want it and that's it - period.
@weoreference bit misunderstanding in my questions. I was asking about: how to make tsc understand what the path points to? Is it a js file directly? Or is it a folder with index.ts inside? Or is it a module with package.json that holds main field? Or esnext field? Or any other that is a non-standard? Or is it a path that is rewritten by webpack? Or rollup? If so, where is the config? Is it another bundler maybe? Some less known? And what are the other use cases I did not think of in the above sentence?

Maybe I can pass a flag to tsc/tsconfig, such as “renameImports”:true, or some other signal that gets picked up by tsc.

@weoreference can you not already do this with path mapping? The paths option in TSconfig file gives us the control to map the import paths to JS files. Sorry if I'm interpreting incorrectly.

@Draccoz Thank you for your reply, please let me clarify

And what are the other use cases I did not think of in the above sentence?

Oh yes, it would be very difficult for tsc to have knowledge about all these environment/build tool combinations.

But tsc could have a simple utility that helps developers cover _most_ use cases, and this is the “standardImportExtension” config value that I explained.

Of course, here comes the problem of “guarantee”. Earlier in this thread, it was argued that tsc should “guarantee” that the compiled js will in fact run [on some environment].

Well... maybe this was too hard a commitment. It is, indeed, very difficult for tsc to guarantee the js will run [on some environment], because, as you just described, that environment is very hard to define in the current js landscape.

But the simple flag utility proposed by @borfast solves _most_ of the js import errors encountered by new users trying tsc.

Advanced usage, which as you mention consists in all these considerations —-

Is it a js file directly? Or is it a folder with index.ts inside? Or is it a module with package.json that holds main field? Or esnext field? Or any other that is a non-standard? Or is it a path that is rewritten by webpack? Or rollup? If so, where is the config? Is it another bundler maybe?

— well, that usage, can indeed left to custom user scripts, bundlers and other tools.

But only because the advanced usage is hard to solve does not mean we should avoid solving the easy cases.

And the easiest case is adding a .js extension to imports; which we can solve, and should solve.

@weoreference so I'm downvoted by you for asking a question and trying to be helpful and then you ignore the question. Seems like you just want to argue and debate instead of actually progressing to a productive solution. You've downvoted everyone's attempt at doing so. Last message for me on this thread. Apologies for all the spamming, everyone.

@weoreference look at the very beginning of this conversation, at @DanielRosenwasser comments. He explains why they would not implement it ("at the moment"? useless hope giving). After his statement nobody from typescript commented the topic further. For me this discussion should be closed and Microsoft should officially state their stance. Period.
Just as a side note - personally I'm all for that feature, would simplify a LOT. Lost my hope thought as I also understand typescript team arguments.

@weoreference

Not hypothetical, tsc knows it'll generate it. It is guaranteed to exist, when using tsc.

But there’s a symmetry implied here that you’re not admitting to.

This “guarantee” you mention holds both ways. If, as you say, “tsc knows it’ll generate [js files]”, then it’s an equally valid question to ask: “Well, then why is tsc NOT acting on this guarantee and apply the .js extensions which it knows is missing from the compiled js?”

The symmetry doesn't hold actually. It's not as simple as just "applying a .js extension" - tsc would have to resolve and rewrite the specifier, and resolution may depend on type declarations. TypeScript and Babel support single-module compilation modes (isolatedModules for tsc) and in this case if you import types instead of JavaScript, tsc would have to load the type declarations to determine where the corresponding .js file would be, in order to rewrite the specifier. To keep things consistent and not have edge cases, it's simplest to only support importing the .js files that exist before compilation or are a known result of the current project.

@justinfagnani @Draccoz Thank you; alright, yes I understand.

My final comment: "only because hard cases can't be solved does not mean we can't solve the easy cases."

Thank you

I've been under the assumption that TypeScript is going against spec here (and producing invalid JavaScript), but I can't find anywhere in the spec that actually requires that the import path match filenames exactly (with extension). It's very hard to digest, so I'm not completely confident, but all I can find is that the spec requires the 'ModuleSpecifier' in the 'FromClause' to be a 'StringLiteral'. Seems a very loose definition, but I suppose that may be to allow flexibility in module resolution in the many different environments in which ECMAScript exists. Can anyone confirm whether I am reading this correctly? If this is really the case, I'd have to soften my stance towards the way TypeScript handles this. Although, I'd still prefer there be more interoperability between TypeScript, Node, and the web. The current situation is not ideal.

I came to this issue after reading this. That issue gave me the impression that Node's system operates the way it does (strictly matching filenames) due to the ES spec, but I can see now that I misread that (they were not referring to the ES spec), and perhaps neither Node, nor TypeScript are actually doing anything 'wrong'. Then, this issue is really about incompatibilities between equally valid approaches to module resolution between different environments. Still, clearly TypeScript's main target environments are Node and the web, and although Node could theoretically change their approach, I don't think it's very likely that browsers will, so I still think this issue is valid.

Edit: I think it might be this section of the spec which seems to specify that module resolution is 'host-defined'?

@kj the relevant spec text is in the HTML spec: https://html.spec.whatwg.org/multipage/webappapis.html#resolve-a-module-specifier

It says that import specifiers must be full URLs or absolute or relative paths. If the specifier doesn't parse as a URL (usually beginning in http:// or https://) or begin with /, ./, or ../, an error is thrown. Path specifiers are resolved against the importers's base URL.

No other path searching or resolution is done. This means that the specifier must resolve to the full URL of the module and you have to include the extension if the server requires it. Servers do have the option of returning responses for extensionless URLs.

The reason why Node went with a more restrictive resolution algorithm for modules is to be more compatible with the web, so that it's more common that modules published to npm don't need extra tooling to run natively in browsers. Importing by package name is still a very important feature, so Node does support that, but aiming to be compatible with the Import Maps proposal (which Deno and SystemJS already support).

All told, tsc's support for importing with .js extensions works perfectly in both Node and web browsers. It's actually the extensionless imports that cause problems without additional tools. Usually though, the tools that support Node resolution for package names will also use class require() style resolution and resolve relative paths and add their extensions too. This is what es-dev-server does.

Thanks @justinfagnani. So at least in the browser it's really just up to how the server implements it. That's good to know. This is still fairly unintuitive and confusing and I do believe there needs to be more convergence between projects for the good of the community, but I'm no longer sure what that might look like.

Edit: For now, this will be enough for me (especially given the explanations above, I feel more comfortable with it):

https://nodejs.org/api/esm.html#esm_customizing_esm_specifier_resolution_algorithm

I'm not sure if this option is referenced in TypeScript documentation, but if it's not, perhaps it should be to avoid confusion. It doesn't seem to be, searching either 'site:typescriptlang.org specifier-resolution' or 'site:staging-typescript.org specifier-resolution'. Although, as this is still pretty new in Node, that's not too surprising.

@justinfagnani I agree that the .js extension just works in most cases, except for cases where someone takes an ideological stance _against_ it (e.g. https://github.com/TypeStrong/ts-node/issues/783, which you're familiar with).

I made a dependency-free version of @quantuminformation's script over here. That version also contains a regex that will only replace modules that do not end with .js already.

same need

Achieving .js extensions for ESM compatibility by using .js in the import statements is not working for me. When I import a TS file and omit any extension, it compiles fine. When I add a .js extension to the import, I get lots of errors (that shouldn't exist) from the TS compiler.

It seems the main reason that this feature isn't being developed is due to the difficulty/impossibility of compatibility with various bundlers, but Node.js and bundlers developed their own module schemes in the absence of support for modules in ECMAScript. Now that we have ESM, as inadequate as some people may consider it to be, it's the way forward. As more code is developed in ESM and web components, the use of bundlers will decrease, and this pain point between TS and ESM will develop more friction. It would be good, even if the initial support is buggy and doesn't consider all use cases, if it could start to be supported.

Until there is a better solution this is a no-dependencies node script to use as build task

configure in you package.json:

  ..
    "scripts": {
        "build": "node build.js",
   ...

//build.js
import { execSync} from "child_process"
import * as util from "util"
import * as fs from "fs"
import * as path from "path"

//function to recurse dirs finding files
function fromDir(startPath, filter, callback) {

    //console.log('Starting from dir '+startPath+'/');

    if (!fs.existsSync(startPath)) {
        console.log("no dir ", startPath);
        return;
    }

    var files = fs.readdirSync(startPath);
    for (var i = 0; i < files.length; i++) {
        var filename = path.join(startPath, files[i]);
        var stat = fs.lstatSync(filename);
        if (stat.isDirectory()) {
            fromDir(filename, filter, callback); //recurse
        }
        else if (filter.test(filename)) callback(filename);
    };
};

//this add .js to lines like:  import .* from "\.  <-- only imports from ./ or ../ are touched
function addDotJsToLocalImports(filename) {
    var buf = fs.readFileSync(filename);
    let replaced = buf.toString().replace(/(import .* from\s+['"])(?!.*\.js['"])(\..*?)(?=['"])/g, '$1$2.js')
    if (replaced !== buf.toString()) {
        fs.writeFileSync(filename, replaced)
        console.log("fixed imports at "+filename )
    }
}

//------------------------
//---BUILD TASK START 
//------------------------

execSync("npx tsc --build -verbose", { stdio: 'inherit' })

//add .js to generated imports so tsconfig.json module:"ES2020" works with node
//see: https://github.com/microsoft/TypeScript/issues/16577
fromDir("./dist", /\.js$/, addDotJsToLocalImports)

based on https://github.com/microsoft/TypeScript/issues/16577#issuecomment-310426634

Can anyone remember what they were doing 3 years ago when this issue was opened?

The non-bundler solution is to write web-distributed projects with .js as the extension.

That 100% works. The bigger problem is trying to write modules that target web and Node.js, but that's a problem for JS in general.

If there was something to be done, it'd be adding --moduleResolution=web, but that's not the scope of this issue here.

I had to write this post-build script to add the .js extension.

fix-ts-imports

#!/usr/bin/env sh

# Fixes JavaScript module imports generated by TypeScript without extension.
# Converts
# import {} from './module'
# into
# import {} from './module.js'
#
# EXAMPLE
# ./fix-ts-imports

ProjectDir="$(cd "$(dirname "$0")/.." && pwd)"

fix() {(
        local pkg="$1"
        shift

        find "$pkg" -type f -iname '*.js' -not -ipath '*/node_modules/*' -print0 \
        | while read -r -d '' file; do
                sed -i '' -E 's|(import .+ from ['\''"]\.?\./.+[^.][^j][^s])(['\''"])|\1.js\2|g' "$file"
        done
)}

if test $# -eq 0; then
        set -- "$ProjectDir"
fi

for pkg; do
        fix "$pkg"
done

I have similar script, in JavaScript:
https://github.com/yoursunny/NDNts/blob/743644226fe18d48e599181e87ad571a2708a773/mk/build-post.js

It's invoked as:

tsc -b mk/tsconfig-solution.json -w --listEmittedFiles \
  | node mk/build-post.js

The major drawback for this kind of scripts is that they break source maps, so that debugging becomes less effective.

using modern tooling and .js extensions in source, everything just works excellently in node and browsers

  • typescript
  • es modules
  • rollup

i can only assume folks having troubles here are actually stuck in some broken hybrid of es-modules and node resolution, as developers cling to familiar landmarks of their old workflows — probably webpack is to blame...

— probably webpack is to blame...

And so — the cat is out of the bag.

using modern tooling and .js extensions in source, everything just works excellently in node and browsers

_Most_ things work excellently, and I agree that source files should all have .js extensions in their exports. In my experience, the one thing that doesn't work well with this is ts-node, due to their stubborn refusal to support .js extensions. Yes, you can add a pre-transpilation step before running node, and the .js imports will work, but if you want to run .ts code or tests directly with node and also in the browser, you're currently mostly out of luck. (To clarify, I think this is a ts-node bug, not a TypeScript bug).

Thanks @chase-moskal.

I refactored the repo and the tsconfig file, and now
import {something} from './something.js'
does not throw
typescript force overwrite error TS5055: Cannot write file because it would overwrite input file
anymore, and I don't need the fix-ts-imports hack anymore.

In 250+ comments, there's been very little clarity. To summarize:

Background

Browser modules

Browsers resolve EcmaScript modules according to URL, including relative URLs. (WHATWG)

Node.js modules

Node.js resolves modules (both EcmaScript and the Node.js-specific CommonJS) via a far more complex algorithm that involves multiple fallbacks and parsing package.json files. (Node.js) That can be customized, e.g. with --experimental-specifier-resolution=explicit which requires full path.

TypeScript modules

TypeScript has multiple available module resolutions algorithms and a variety of options to further customize these. (TypeScript) The intent is that users write the same module specifiers as are used in the produced output, adjusting tsc's resolution with options such as baseUrl and pathMappings.

In practice, most users use the node moduleResolution, targeting a Node.js environment or compatible bundler. This request focuses on users targeting browsers without a bundler.

ts-node module resolution

Apparently, ts-node doesn't support module identifiers with extensions. Though it's unclear why, since both Node.js and TypeScript do, and ts-node is ostensibly the amalgamation of those.

Facts

Fact 1: You can use .js extensions

For broader compatibility (namely browsers), you can use .js extensions today. With the odd exception of ts-node (IMO bug), everything works right now by specifying the full path (that is, including extension).

Fact 2: It's not as simple as "add extension"

This request is better summarized as "transform the module identifier to file path." E.g. ./example becomes ./example/index.js and 'lodash' becomes '.node_modules/lodash/index.js'.

Note that sometimes there isn't even a resolved file path, like with ambient module declarations.

declare module "lodash" {
}

Perhaps the module rewriting is limited to TS modules in the current project/compilation.

In any case, we're now we're violating one of the design parameters of TS, that other modules affect the output for the current one.

Conclusion

It's possible to use path for module identifiers that work for web. (E.g. ./foo/index.js instead of ./foo.)

(Though practically speaking, you'll likely want a bundler anyway target browsers. Namely, if npm packages are used.)

@pauldraper there's an important problem with "Fact 2":

This request is better summarized as "transform the module identifier to file path." E.g. ./example becomes ./example/index.js and 'lodash' becomes 'node_modules/lodash/index.js'.

You really don't want to resolve specifiers outside your package at compile time, because these may not be the paths available when the package is installed somewhere else. You need to run Node module resolution in each unique package installation. Otherwise we could write and publish import {} from './node_modules/lodash/index.js' and be done with it.

@justinfagnani , I agree, but that demonstrates that module identifiers are abstract and you are manipulating locations of modules outside tsc. And that tsc cannot/does not need to concern itself with such manipulations.

This request is better summarized as "transform the module identifier to file path." E.g. ./example becomes ./example/index.js and 'lodash' becomes '.node_modules/lodash/index.js'.
Note that sometimes there isn't even a resolved file path, like with ambient module declarations.

That not what this request is about. It is about adding extension when emitting code. We want to write modules compatible with type: "module" without additional hacky build step. External modules like lodash would continue to work even without .js as these are CommonJs.

Example:

// ./src/moduleA.ts
export const test = 2;
// ./src/moduleB.ts
import {test} from './moduleA'



md5-ec0300a1c6d92a03c70699d0e52c0072



```js
// ./lib/moduleB.js
import {test} from './moduleA.js'

In addition to above typescript do not support package.json "exports" and "type". There is no path towards true ESM in projects I manage.

This request focuses on users targeting browsers without a bundler.

That's incorrect. I rarely write TS/JS for browsers, I mostly work on server-side code and I also need this because I want to use ESM loading in node and not depend on bundlers and extra code for module loading.

External modules like lodash would continue to work even without .js as these are CommonJs.

Unless they aren't.

When emitted.

It might be ./moduleA.js. Or it might be ./moduleA/index.js, right? Node.js module resolution permits a number of paths.

Unless they aren't.

Can you provide example when these would not work? Node.js just landed support for importing named export from CommonJS.
https://nodejs.org/api/esm.html#esm_import_statements

It might be ./moduleA.js. Or it might be ./moduleA/index.js, right? Node.js module resolution permits a number of paths.

Not in ESM mode. index.js would no longer be supported as it is now. ESM loader would read package.json metadata "exports" and "type".
https://nodejs.org/api/esm.html#esm_mandatory_file_extensions

For typescript + node.js based projects we need better native ESM story. This can happen via tooling (typescript) or in node.js Problem is that there is lack of consensus on what should be done.

Edit removed link to node PR as it was misleading.

Was this page helpful?
0 / 5 - 0 ratings