29

All major browsers have supported ES6 modules for some time.

These differ from many of the server-side approaches in that they need to specify the exact file to import from - they can't use file discovery.

This makes sense - in Node applications or bundlers like WebPack they only really need the name of the module, and then can spend a bit of extra time discovering the specific file that holds the code. On the web that could be a lot of wasted round trips (is 'library' in library/index.js, or library/library.js, or library.js? require() doesn't care but on the web we have to).

TypeScript has ES6 modules support (set "module": "es6" in tsconfig.json) but it appears to be using a file discovery approach...

Suppose I have library.ts:

export function myFunction(...) {  ... }

Then in app.ts:

import {myFunction} from './library';
var x = myFunction(...);

However, this is unchanged when transpiles - the TS output still has the 'library' name for file discovery, which doesn't work. This throws an error because 'library' isn't found:

<script type="module" src="app.js"></script>

In order for ES6 modules to work the TS output needs to reference the specific file:

import {myFunction} from './library.js';
var x = myFunction(...);

How do I make TS output valid ES6 module import statements?

Note: I am not asking how to make a bundler join the TS output into a single file. I specifically want to load these files individually using <script type="module">

6
  • 1
    ...from 'library'; and ...from './library.js'; are two different things, the first is named package, the second is relative path to a module Commented Aug 29, 2017 at 7:10
  • @MaximKoretskyi yes - the named package has an entry point and that needs to be the explicit relative or absolute path from the module import. The web can't use package names. Commented Aug 29, 2017 at 7:15
  • the TS can't output relative path if you used named package import because the library is not in the the same directory as the importing module, right? Commented Aug 29, 2017 at 7:23
  • @MaximKoretskyi that doesn't matter, the path doesn't work either way. ./library fails, ../src/utils/library fails, https://example.com/cdn/library fails. Commented Aug 29, 2017 at 7:25
  • fails when? in a browser? Commented Aug 29, 2017 at 7:27

3 Answers 3

24

This is a bug in TypeScript, though there's some debate about whether it should be fixed.

There is a workaround: while TS won't allow you to specify a .ts file as the source of a module, it will let you specify a .js extension (and then ignore it).

So in app.ts:

import {myFunction} from './library.js';
var x = myFunction(...);

This then outputs correctly in app.js, and TS has found the import definitions and bindings correctly.

This has one advantage/gotcha to be aware/careful of: TS just ignores the .js extension and loads the rest of the path with the usual file discovery. This means that it will import library.ts, but it would also find definition files like library.d.ts or import files in a library/ folder.

That last case might be desirable if you're joining those files together into a library.js output, but to do that you're going to be looking at either lots of nested tsconfig.json files (messy) or possibly the pre-transpiled output of another library.

Sign up to request clarification or add additional context in comments.

1 Comment

If you use Visual Studio Code you can configure it to automatically add the .js-ending when auto-importing modules. Go to settings, then type "module specifier" in the search field. You find option "typescript > Preferences: Import Module Specifier Ending" which you can set to ".js".
3

The compiler takes a module kind flag:

--module ES2015

And you'll also need to be targeting ECMAScript 6 / 2015...

--target ES2015

You need both the module kind and the compilation target to be ECMAScript 2015 minimum to have "zero transformation imports".

Your import statements should look half-way between your two examples:

import {myFunction} from './library';

Additional Notes

There is still clearly a lot of discussion about module resolution... there is the TC39 specification, and the WHATWG specification - plus Node is currently still file-extention-less... looks like RequireJS might live longer than we all thought... please see:

The TypeScript thread for supporting file extensions during import transpilation (i.e. will it add the file extension?).

Recommendation

Stick with a module loader, for example RequireJS or SystemJS. This also means your modules can be shared between browser and server by using UMD or System module kinds repectively.

Obviously, once the ECMAScript discussion reaches a conclusion this will need a revisit.

11 Comments

That's a little better, but doesn't work. Try it in a supporting browser (say Canary) and you get a 404 for the non-existent file "library". The TS output needs to specify the file to meet the ES6 module spec, so import {myFunction} from './library.js' <- that .js at the end is required.
"No .js extension is needed" - that depends entirely on the filename under which the script is stored.
@Bergi That's exactly my point. In the ES6 module spec that's close to being implemented by all the major browsers that .js is needed. If you enable ES6 modules (either by --module ES2015 on the command line or by "module": "es6" in the config) then TS transpiles library.ts to library.js - so the compile tools know exactly the specific file, the browser doesn't.
@Keith Ah, I didn't think the transpiler knew to mess with module names.
No - I don't think we agree on that. I work on many libraries / frameworks that are not targeted specifically at Node, or at Browsers - so I write ES style imports and have TypeScript transpile them using the UMD flag. This allows me to package the libraries as .js + '.d.ts` packages that can be used either in browsers, or on Node, and not just from other TypeScript programs. We want to be in a situation where we can write a module independent of where it will run.
|
1

For a personal project I went the other way. Since I had NPM calling a shell script to copy index.html over to the /build folder, I had the shell script then mass-rename all .js files to have no extension at all.

I did have to inform IIS in "MIME Types" section that an extension-less file should have MIME type application/javascript for that particular site, but it did indeed work. No webpack, no SystemJS, nothing. Index.html just had a hard-coded

    <script type="module">
      import "./app";
    </script>

This was important because I was using a testing framework jest which did not like me putting the .js into the typescript import statements.

1 Comment

Sorry, old response that I missed at the time. Yes, you can use server redirects to fix this too, but that's not really the question I was asking - I want the TS output to be right, not hack around it with server routing.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.