Reputation: 341
I am trying to transpile my TypeScript project into JavaScript, however, something doesn't seem right. I configured the project to resolve as an ES6 Module (aka ESM), via the "module":"ES6"
setting, but it doesn't solve the issue.
tsconfig.json
configuration looks like: {
"compilerOptions": {
"module": "es6",
"target": "es6",
"lib": ["es6"],
"sourceMap": true,
}
}
I wrote a simple test-case senario using two modules.
The first module — module1.ts
— only exports a constant, as shown below:
export const testText = "It works!";
The second module — main.ts
— just imports the export from the first module:
import { testText } from 'module1';
alert(testText);
The output file of the second module (or main.js) is embedded in my index.html
document, and I have added the type-attribute as type="module"
to the <script ...>
tag, as is shown below:
<script src="main.js" type="module"></script>
When I test this with either Firefox (dom.moduleScripts.enabled
is set in about:config) or Chrome Canary (Experimental Web Platform flag is set) it doesn't work.
The Typescript compiler seems to transpile the TS import { testText } from 'module1';
-statement to the JS statement import { testText } from 'module1';
. (Note: both are exactly the same)
The correct ES6 import-statement would be:
import { testText } from 'module1.js';
(Note the .js file extension)
If I manually add the file extension to the generated code, it works.
Did I do something wrong or does the Typescript "module": "es6"
setting just not work correctly?
Is there a way to configure the tsc in such a way that .js file extensions are added to the generated import statements?
Upvotes: 34
Views: 16142
Reputation: 11879
NOTE FROM AUTHOR: "The date that this answer was published is different than the date that the answer was originally created on. Due to changes in TypeScript, I have had to write a new answer twice. Because dates are extremely relevant in this post, I have included them in a couple places, in-case the answer gets stale. This will help readers know when I was referencing certain versions of any technologies that I have written about below."
ESM was not an easy technology to incorporate into Node. And when it came to writing Node ES-Modules in the TypeScript language, well, that actually looked like, at a certain point in time, that it could never happen. IMO, it seemed like certain projects, didn't want to do what they, now have done, to support the Standard in the Node Back-end RTE. Anyhow, lets move on...
Initially when I answered this question I suggested that people who are experiencing this issue use the following flag:
node --es-module-specifier-resolution=node
The flag worked, or worked as the best solution up to this point (or well up to TS 4.7 Nightly Release). IT SHOULD BE NOTED That depending on the environment, and the project, that you are working in, this will still be the best solution for now.
At this Point in Time Much has Changed:
During the Nightly releases of v4.6 TypeScript added support for new module & moduleResolution specifiers, which are ("obviously") set in the tsconfig.json
. They were released during the v4.6
nightly builds, just a little while before v4.6 beta was released, and 4.7 became the nightly build. Now, v4.7
is beta, and v4.8
is the new nightly build (I mention all this version stuff, just to make sure your up to date).
"module": "NodeNext"
"moduleResolution": "NodeNext"
The reason I say this is because, the new TSC-flags (aka tsconfig settings) were available in v4.6
, which, if you remember, I stated a couple paragraphs ago, however; TypeScript decided not to release them in the current latest version (typescript@latest
) version which is v4.6
. So, they will be included in TypeScript officially, when v4.7
is no longer in beta, and becomes the latest version.
They should always be used with "esModuleInterop": true
, (unless somethings change on the "Node.js" side of things. The "esModuleInterop" setting eases support for ESM, and some of the node modules will need it to work properly with the new ESM import/export module system.
This is the ESM tsconfig.*.json
configuration template, which I add as a starting point to each new project.
"compilerOptions": {
// Obviously you need to define your file-system
"rootDir": "source",
"outDir": "build",
// THE SETTINGS BELOW DEFINE FEATURE SUPPORT, MODULE SUPPORT, AND ES-SYNTAX SUPPORT
"lib": ["ESNext"],
"target": "ES2020",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"esModuleInterop": true, // Eases ESM support
"types": ["node"],
"allowSyntheticDefaultImports": true,
}
module
& moduleResolution
SpecifiersSo you have a couple different TS versions you can use to support the new configurations. You can either use Beta or Nightly. Below is a chart that demonstrates the 3 available versions. As of right now, typescript@latest
is @ v4.6
, and does not support the new configurations.
TS RELEASE | VERSION | NPM COMMAND TO EXECUTE | HAS SUPPORT? |
---|---|---|---|
TS Latest | v4.6 |
npm i -D typescript@latest |
NO |
TS Beta | v4.7 |
npm i -D typescript@beta |
YES |
TS Nightly | v4.8 |
npm i -D typescript@next |
YES |
You need to configure your environment to use the new TypeScript version. I obviously cannot document ever editor's configuration, but V.S. Code seems to be the most common for TypeScript, and V.S. 2022 uses a similar configuration.
In VS Code, you will add the following configuration to your workspace ./.vscode/settings.json
file.
// "./.vscode/settings.json"
{ "typescript.tsdk": "./node_modules/typescript/lib",}
(Which, for this answer, is Node.js)
But why is this so? Why do I need to configure my RTE? Whats the deal??? Doesn't the RTE know everything?
**But didn't we already Configure the tsconfig.json
file?
We need to tell node that we are going to be executing an ECMAScript Module, and not a CommonJS module. We can do this one of two different ways.
package.json
file using the package.json
configuration file's "type" field. // @file "package.json"
{
"name": "project-name",
"version: "2.4.16",
"type": "module", // <-- Lets env know this project is an ESM
// the rest of your package.json file configuration...
}
Alternatively, you can do the some thing as above, but by using the --input-type "module"
flag
Lastly, you can do, what I prefer to do, and just create each file using the ESM file extensions. In js it is .ejs
, and in typescript it is .ets
.
index.ets
and TSC will emit index.ejs
.Lastly, its IMPORTANT that you understand how imports work when writing an "ES-Module". I will list some notes, I think that will be the best format for this info.
You have to use JavaScript file extensions when importing files.
ESM imports are imported using URI's, and not by use of "possix filepaths". The opposite was true for CJS Modules.
Special chars must be percent-encoded, such as # with %23 and ? with %3F.
TypeScript won't make you prepend the MimeType, but ESM uses prepended data types so its a best practice to do so.
For example, its a best practice to import from Node API's & Libraries using the following URI format:
import EventEmitter from 'node:events';
const eEmit = new EventEmitter();
Upvotes: 19
Reputation: 155802
This is a confusing design choice in TypeScript.
In the short term you can work around it by specifying the output file:
in main.ts
specify the .js
extension and path:
import { testText } from './module1.js';
alert(testText);
This will pick up module.ts
correctly, but output with the .js
extension included.
Note that you also need to prefix local files with ./
as 'bare' module names are reserved for future use.
Upvotes: 25