Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Typescript compiler is forgetting to add file extensions to ES6 module imports?

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.


This is what my tsconfig.json configuration looks like:
  {
    "compilerOptions": {
      "module": "es6",
      "target": "es6",
      "lib": ["es6"],
      "sourceMap": true,
    }
  }

Test Case Using a Pair of Modules:

I wrote a simple test-case senario using two modules.

  1. The first module — module1.ts — only exports a constant, as shown below:

    •   export const testText = "It works!"; 
      
  2. 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?

like image 678
Chaos156 Avatar asked Jul 07 '17 21:07

Chaos156


2 Answers

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.

like image 85
Keith Avatar answered Oct 14 '22 18:10

Keith


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."

Originally Authored in December of 2022

The "es-module-specifier" Flag Fix for TS, ESM, NODE stacks


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
(FYI: The solution came from the link attached to the snippet)

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.


However, a Better Solution has been Made Available.


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).

These are the new specifiers/settings that TS has given to us
  1. "module": "NodeNext"
  2. "moduleResolution": "NodeNext"
It has been Announced that v4.7 will include the new settings

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,
    }

Written on: MAY 22nd, 2022 8:50am PST

Gaining Support for the module & moduleResolution Specifiers



So 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

Configure your Development Environment to use the Latest Ver

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",}


Configuring Your Runtime Environment

(Which, for this answer, is Node.js)


But why jD3V? Why do I need to configure my RTE? Whats the deal??? Doesn't the RTE now everything?

  • No, the RTE doesn't just automatically know everything. It has to have some sort of instructions, or clues.

**But didn't we already Configure the tsconfig.json file?

  • Yes, but that is for TypeScript, now we need to make sure node is ready for our TS configuration.

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.

  1. The first is to configure our 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...
    }
  1. Alternatively, you can do the some thing as above, but by using the --input-type "module" flag

  2. 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.

For Example, your index file will be 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.

  1. You have to use JavaScript file extensions when importing files.

  2. ESM imports are imported using URI's, and not by use of "possix filepaths". The opposite was true for CJS Modules.

  3. Special chars must be percent-encoded, such as # with %23 and ? with %3F.

  4. 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();
like image 8
j D3V Avatar answered Oct 14 '22 18:10

j D3V