Typescript: Documentation on Modules is missing ".js" suffix in import statements

Created on 22 Sep 2018  ·  17Comments  ·  Source: microsoft/TypeScript


TypeScript Version: 3.1.0-dev.201xxxxx


Search Terms:
import, module
Code
When using the import declaration from the documentation

import { ZipCodeValidator } from "./ZipCodeValidator";

the latest Chrome will not find the 'ZipCodeValidator' file. The console will show an error "Failed to load resource: the server responded with a status of 404 (Not Found)".

This can be easily fixed by adding the ".js" suffix to the imported module.

import { ZipCodeValidator } from "./ZipCodeValidator.js";

// A *self-contained* demonstration of the problem follows...
// Test this by running `tsc` on the command-line, rather than through another build tool such as Gulp, Webpack, etc.

Expected behavior:
Module should be imported.

Actual behavior:
Latest Chrome returns "Failed to load resource: the server responded with a status of 404 (Not Found)"

Playground Link:

Related Issues:

Question

Most helpful comment

I'm having the same problem, but succinctly, it's that when I compile

import {Foo} from "./module"

and target ES2015 or ESNext modules, it gets transpiled as:

import {Foo} from "./module"

Yes, exactly the same thing. Since I'm targeting javascript, it should be adding the .js on the end, but since it doesn't, any browser that supports modules gets a 404 back from the server, since the transpiled output is actually module.js.

Smells like a bug to me. I'm on Typescript 3.1.3.

All 17 comments

@griii2 I think there's a couple of things going on here:

  1. TypeScript isn't meant to be run in the latest Chrome. You have to compile it first. I think what you want, is to update this issue to be a proposed enhancement for the generated output for import paths when compiling to ES2015 modules, so that files that don't have extensions will be given .js extensions in compiled output.
  2. The file you're importing really is a .ts file, not a .js file! Though you can import .js with allowJs: true or if the .js file has a corresponding .d.ts file, TSC's preferred extension is .ts.

Hi @bcherny, I am not sure I understand what you mean. Of course I compiled the TypeScript files. No, I am not running them in Chrome. What I run in Chrome is an HTML5 file that loads the compiled .js files.

My point is this:

1/ I am reading the documentation: https://www.typescriptlang.org/docs/handbook/modules.html
2/ It briefly talks about ComonJS, AMD, ES2015 modules and more.
3/ None of the examples are labeled as specific for one of the module types.
4/ I recreate one of the examples in ES6 and it doesn't work.

@griii2 Sorry, I don't know what you know or don't know - always better to check :)

See what I said above - is this your intention?

I think what you want, is to update this issue to be a proposed enhancement for the generated output for import paths when compiling to ES2015 modules, so that files that don't have extensions will be given .js extensions in compiled output.

I see, you are saying the compiler should work the way the documentation says. I will raise the feature request.

But as things stand today, the documentation does not match the implementation, so I suggest that until the feature request is released the documentation should be fixed to describe the implementation.

The exact resolution of module specification -> file is up to the host; it is likely that nothing we write will work in every possible host. The examples work when targeting CJS compilation, which is still the default and is widespread.

Hi @RyanCavanaugh:

a) If the example works only when targeting CommonJS then the documentation should not imply it works for all module targets.

b) As a consequence, module importing targeting ES6 is not documented today.

Anyway, this was my feedback to the TypeScript team because I care. If you want to be defensive instead of fixing what is broken, that is up to you.

Best
Daniel

I'm having the same problem, but succinctly, it's that when I compile

import {Foo} from "./module"

and target ES2015 or ESNext modules, it gets transpiled as:

import {Foo} from "./module"

Yes, exactly the same thing. Since I'm targeting javascript, it should be adding the .js on the end, but since it doesn't, any browser that supports modules gets a 404 back from the server, since the transpiled output is actually module.js.

Smells like a bug to me. I'm on Typescript 3.1.3.

As an aside, this is handled in SystemJS with a defaultExtension: 'js'. Is this something that Chrome, Safari, Edge and other <script type=module> compliant browsers are meant to be doing? If yes, then okay, but in the meantime, can we have a --moduleDefaultExtension flag or tsconfig setting please?

I have the same problem: When targeting "es6" then import TransformTool from "./TransformTool"; is compiled to import TransformTool from "./TransformTool";.
This leds to a 404 "Page not found" error when loading in the browser since it only finds the module if it is imported with import TransformTool from "./TransformTool.js";.
I now manually fix my files each time after compiling. Fixing this problem is really urgent!

@oising It's not really about adding extension, but expanding the full relative path during compilation.

1. What you imported may not be a file but folder.

import { Foo } from './module'

Might also result to:

import { Foo } from './module/index.js'

The path cannot be determined by the entry, but require the full path of the dependency.

2. The target JavaScript file compiled from TypeScript may not be .js

When using jsx: preserve, a module.tsx file would be compiled to module.jsx.

3. There could be complex path mapping

Whenever there's a package.json, the path could actually be path.resolve(__dirname, packageJson.main). And the search path can be ./node_modules, ../node_modules, etc, need to be prepended.

Also needs to take compilerOptions.paths into consideration.

4. Declaration path and JavaScript path may be inconsistent

What being imported may not be a .ts(x) file, say that you have the following source file:

- module.d.ts
- module
  - index.js

For TypeScript only .d.ts is checked, the resolution path is ./module.d.ts, but for engines or bundlers, the JavaScript file used is ./module/index.js, which is not even in the same folder of the declaration file. For package.json it's even more complicated as declaration is specified by types/typings field.

To get the right path, there must be an additional kind of path resolution which ignores declaration.

@trotyl Sure, it's complicated with Node (the majority of your examples), but if you're just wanting to write JS modules that directly target browsers - without using bundlers like webpack or parceljs, it shouldn't be that hard.

@oising All of the points above are problems for not using Node.js or any bundler.

It would be great if we could just have the _option_ of being able to specify something like resolveImportFiles so that the exact references output file is what is used, this would work for jsx, js, or whatever extension is used. Typescript already knows what an import is referencing, as it does type checking against it. Within typescript you use typescripts module resolution process, within esnext, typescript should use esnext's module resolution process, there could be a variant for node, or the browser, so would be dependent on the target engine, esnexts module resolution process works on full URI values, or relative references.

If typescript was to be used to do this resolution ahead of time, it could be powerful for a lot of reasons.

For example given this directory:

/util/store.js
/util/index.js
/feature-a.js
/feature-b.js

store.js is referenced by /util/index.js as export * from "./store"
index.js is referenced by /util/feature-a as import { Store } from "./util"
index.js is referenced by /util/feature-b as import * as Util from "./util"

This could be resolved universally as when transformed into esnext as:

export * from "./store.js"
import { Store } from "./util/store.js" <- Notice the shortcut, which we already know about!
import * as Util from "./util/index.js"

This is going to work universally because it sill follows a resolution process that is compatible with all engines, I don't believe there is anything lost by doing so, the option on compilerOptions could be:

type resolveImportFiles = boolean | false;
  • If false or not provided, retain current value, no breaking change
  • If true, resolve as file if referenced file, if vendor module then retain current value (this works for the browser as people can utilise import maps, so no additional of rewriting the wheel), no breaking change, as user must change this manually

This would be a semver minor change as well.

This can be extended though, and this is where it gets powerful.

Lets say we defined it as:

type resolveImportFiles = "unkpg" | boolean | false;

Then when we reference preact, we and the reference is a vendor module then we use https://unpkg.com/[email protected]/src/preact.js, else when we're referencing the local file we get the same as if resolveImportFiles was true, this isn't a requirement of resolveImportFiles though and more of an extension of the idea.

Hello, i encountered the same problem, and i SOLVED "easily", this way : URL REWRITING, what do i mean ? i'm gonna explain that from the beginning !

Here is the story, from compiled Typescript code, i wanted to obtain some ES6 code(target es2015 in tsconfig.json), in order to keep in the final code the original syntaxes: import/export that i've written in Typescript ( indeed at final, i did not want to use Module loaders for ES5 !! Because it's old school...).

The thing is that, when Typescript generates the ES6, it completely keeps intact the syntaxes : import .. from "./.../MyJSFile", so it keeps it without the ".js" extension !! And of course, in Typescript code, you can't mention the extension on an import syntax ! (it assumes it to be ".ts").

So now, i've got like you, the ES6 code generated, and containing import syntaxes that DOESN'T mention the ".js" extension of the imported files. So what is the problem ?

The problem is :

  • In order to execute such code in a browser, because it uses ES6 Modules
    you HAVE TO run it from a WEB SERVER (no matter which) !

  • BUT ! any Web Server won't find the files you want to import
    (import ... from "./.../MyJSFile"), because without extension, it says, well
    this resource doesn't exist ! and indeed it doesn't !! BUT what we know is
    that the resource "./.../MyJSFile.JS" EXISTS !
    So ?? You see me coming ?

    YOU JUST HAVE TO WRITE : URL REWRITING RULES FOR THE WEB SERVER, and that's
    what i did to solve ! I did it with :

    • an APACHE WEB SERVER (that has a directive : AllowOverride All ,
      in order to allow URL rewriting !!).
    • and then, i just created an .htaccess file at the root of my
      localhost site, in which i tell him:
      If the requested resource doesn't exist, then try to find
      (by URL Rewriting), the same resource name BUT, with a ".js" extension
      at the end of it ! AND IT WORKS FINE !! Here is my .htaccess in its
      simplest working form :

      RewriteEngine on
      
      RewriteCond %{REQUEST_FILENAME} !-f
      
      RewriteRule  ^(.*)(/[A-Za-z0-9_]+)$  $1$2.js   [L]
      

AND THAT's really all you have to do ! with an Apache Server.
So keep in mind, whatever is the Web Server, the idea is : URL REWRITING !

I ALSO USED THE SAME PRINCIPLE WITH a NODE.JS WEB SERVER that i've created, here is its full tested code, it works absolutely fine (nodeJsAsWebServer.js) :

//In terminal, just run : node nodeJsAsWebServer  
//to launch this below Web Server. (CTRL+C to stop) .
//
//And then in the browser, just invoke your ES6 code 
//(via normal html call), 
//on this domain : localhost:555

//------- nodeJsAsWebServer.js ---------

const oHttpTool = require('http');
const oFilesTool = require('fs');
const oPathesTool = require('path');

const oMIMETypes = {
  'html': 'text/html',
  'css': 'text/css',
  'js': 'text/javascript',

 'json': 'application/json',

  'png': 'image/png',
  'jpg': 'image/jpeg',
  'ico': 'image/x-icon',

  'wav': 'audio/wav',
  'mp3': 'audio/mpeg',

  'pdf': 'application/pdf',
  'doc': 'application/msword'
};

let oMyWebServer = oHttpTool.createServer(function (req, res) {

  console.log("\n\n=========================================================");
  console.log("=========================================================\n");

  let sSubUrl = req.url;
  console.log("Client code asks for: "+sSubUrl);

  let sFileExtension = oPathesTool.parse(sSubUrl).ext.substr(1);    
  let sMIMEType = oMIMETypes[sFileExtension];
  console.log("Asked MIMEType="+sMIMEType);

  let sFileHDDPath = __dirname; //Real path on server hard disk
  let sFileToRead = sFileHDDPath+sSubUrl;
  console.log("FileToRead="+sFileToRead);
  oFilesTool.exists(sFileToRead, function (pbFileExists) {
    if(!pbFileExists) {
      console.log(`  - File NOT FOUND on the server!  -`);
      if (sFileExtension==="") { //If the requested URL doesn't have any extension !
        let sNewUrl = sSubUrl+".js"; //<<<<<<<<<<<< URL Rewriting
        console.log(" REDIRECTION ...(sNewUrl='"+sNewUrl+"'; ldUrl='"+sSubUrl+"')");
        res.writeHead(301, {Location:sNewUrl});//Redirect, we try to load this newURL
        res.end();

      } else { //if the URL already had an extension.
        res.statusCode = 404;
        res.end();
      }

    } else { //Normal case, when the requested resource exists.
      oFilesTool.readFile(sFileToRead, function(poError, poData){
        if(poError){
          res.statusCode = 500;
          console.log("ERROR getting file content !!", poError);
          res.end();

        } else { //We send the file whole content and with a header.
          res.setHeader('Content-type', sMIMEType );
          res.end(poData); //<<Also works for Binary files such as Image, PDF, etc..
        }
      });      
    }
  });

});


let iPort=555; //<<< choose your port on localhost.
console.clear();
console.log("Node Web Server listening on localhost:"+iPort+" ...    (CTRL+C to stop)");
oMyWebServer.listen(iPort);

This issue has been marked as 'Question' and has seen no recent activity. It has been automatically closed for house-keeping purposes. If you're still waiting on a response, questions are usually better suited to stackoverflow.

I'm also running into this issue; I think it's plainly almost unimaginable that a simple .ts file using modules and targeting modern browsers and ES modules cannot be compiled to a working javascript file in the browser.

I'm using the workaround suggested by others in the related thread (using an explicit .js module reference in the typescript file, like "import {Foo} from './module.js' ", but this feels rather strange, although it works well in the browser.

This issue has been marked as 'Question' and has seen no recent activity. It has been automatically closed for house-keeping purposes. If you're still waiting on a response, questions are usually better suited to stackoverflow.

This would make a good question for stack overflow if the feature were supported. As a workaround, @RickInfoDev's satirical answer isn't that bad especially if the typescript people will never support Web Modules

Was this page helpful?
4 / 5 - 1 ratings

Related issues

weswigham picture weswigham  ·  3Comments

DanielRosenwasser picture DanielRosenwasser  ·  3Comments

dlaberge picture dlaberge  ·  3Comments

fwanicka picture fwanicka  ·  3Comments

blendsdk picture blendsdk  ·  3Comments