Appending .js extension on relative import statements during Typescript compilation (ES6 modules)
Asked Answered
O

20

198

This seems to be a trivial problem, but it is not very obvious what settings/configurations need to be used to solve this issue.

Here are the Hello World program directory structure and the source code:

Directory Structure:

| -- HelloWorldProgram
     | -- HelloWorld.ts
     | -- index.ts
     | -- package.json
     | -- tsconfig.json

index.ts:

import {HelloWorld} from "./HelloWorld";

let world = new HelloWorld();

HelloWorld.ts:

export class HelloWorld {
    constructor(){
        console.log("Hello World!");
    }
}

package.json:

{
  "type": "module",
  "scripts": {
    "start": "tsc && node index.js"
  }
}

Now, execution of the command tsc && node index.js results in the following error:

internal/modules/run_main.js:54
    internalBinding('errors').triggerUncaughtException(
                              ^

Error [ERR_MODULE_NOT_FOUND]: Cannot find module 'HelloWorld' imported from HelloWorld\index.js
Did you mean to import ../HelloWorld.js?
    at finalizeResolution (internal/modules/esm/resolve.js:284:11)
    at moduleResolve (internal/modules/esm/resolve.js:662:10)
    at Loader.defaultResolve [as _resolve] (internal/modules/esm/resolve.js:752:11)
    at Loader.resolve (internal/modules/esm/loader.js:97:40)
    at Loader.getModuleJob (internal/modules/esm/loader.js:242:28)
    at ModuleWrap.<anonymous> (internal/modules/esm/module_job.js:50:40)
    at link (internal/modules/esm/module_job.js:49:36) {
  code: 'ERR_MODULE_NOT_FOUND'
}

It is obvious that the problem seems to have been originated from the fact that in index.ts Typescript file there is no .js extension in the import statement (import {HelloWorld} from "./HelloWorld";). Typescript didn't throw any error during compilation. However, during runtime Node (v14.4.0) wants the .js extension.

Hope the context is clear.

Now, how to change the compiler output setting (tsconfig.json or any flags) so that local relative path imports such as import {HelloWorld} from ./Helloworld; will get replaced by import {HelloWorld} from ./Helloworld.js; during Typescript to Javascript compilation in the index.js file?

Note: It is possible to directly use the .js extension while importing inside typescript file. However, it doesn't help much while working with hundreds of old typescript modules, because then we have to go back and manually add .js extension. Rather than that for us better solution is to batch rename and remove all the .js extension from all the generated .js filenames at last.

Outstretched answered 28/6, 2020 at 6:34 Comment(11)
Just import {HelloWorld} from "./HelloWorld.js"; TypeScript is clever enough to figure out what you want is HelloWorld.ts during compilation.Milieu
TS does not re-write import paths. This something the TS team has been adamant about it. Write the paths that work at runtime, and if needed configure ts to make those paths work. ( in this case no configuration is needed)Xylidine
@SonNguyen That workaround is already known to the author and it doesn't help as this issue originated due to an old project to manage with hundreds of files and hundreds or thousands of import statements in it. If you have to use that we are already using better workarounds than that.Outstretched
@TitianCernicova-Dragomir Yes, that's certainly arrogant or egotistical and quite rude on the TS team's part. This is such a trivial task and certainly this small feature would be helpful to most developers. At least in our case the issue would be better solved if the compiler can remove the ".js" extension from the generated ".js" filenames itself. That's what we are currently doing -- just removing the extension at last from all the js files and leave up to the server to serve with the correct MIME type.Outstretched
@Outstretched it's not about arrogance it's about design principles. If you paste a piece of Javascript in typescript, there should be no runtime differencea between the two versions. When you start re-writing paths you start to have differences in between the two. But I do get the frustration, it's one shared by a lot of people on the multiple github issues on this topic.Xylidine
@TitianCernicova-Dragomir That design principle is quite out of context and irrelevant for this use case where one-to-one correspondence must exist between the js and ts files.Outstretched
so, at the end there was no response!, i am facing this issue cause i am using TS types, if i use .js in typescript then it does not compile, but i have to manually edit the imports every time i compile, regarding the design principle, a flag would be nice i want my system to behave the way i want and i am not planning to have universal code and i don't care about the principle if my system works the way i want to, i'll see if there is an script for this...Verily
@TitianCernicova-Dragomir You might be correct, but the question has nothing to do with the design principles. We are not questioning the design principles here. We are simply looking for a solution to a problem -- a problem that thousands of programmers were facing (obvious from the current number of views on this question).Outstretched
The intent of the question was to find a solution to the problem, not to argue on the design choices. Most users here looking for a solution do not care whether it is a limitation of the Typescript language itself or compiler or something else. Trying to evade the issue or defend it saying this is how it is supposed to work is not helpful for most of us. TS team could have simply provided a post compilation tool to fix the issue which would have nothing to do with the design of the language. Especially for novice programmers, these unnecessary nuances can be highly discouraging.Outstretched
I published this small script that does the substitution of all local imports and exports in tsconfig.json outDir folder, so it is possible to use tsc to compile es modules without any of those reported issues: npmjs.com/package/fix-tsc-es-imports It can be added to your build scripts after tsc compile.Purlieu
Typescript is clever enough to know to use TS when using .js files but not clever enough to use the correct extension on compilation.. Okay thenPredigest
O
105

The possible work-arounds we have come across are as follows:

  1. Use .js extension in the import:

    For new files, it is possible to simply add ".js" extension in the import statement in TypeScript file while editing. Example: import {HelloWorld} from "./HelloWorld.js";

  2. Extensionless filename

    If working with old projects, rather than going through each and every file and updating the import statements, we found it easier to simply batch rename and remove the ".js" extension from the generated JavaScript via a simple automated script. Please note however that this might require a minor change in the server side code to serve these extension-less ".js" files with the proper MIME type to the clients.

  3. Use regex to batch replace import statements

    Another option is to use regular expression to batch find and replace in all files the import statements to add the .js extension. An example: https://mcmap.net/q/127702/-appending-js-extension-on-relative-import-statements-during-typescript-compilation-es6-modules or similar other answers.

Outstretched answered 28/6, 2020 at 18:57 Comment(9)
the "issue" is a non-issue. the compiler does not deal asymmetrically with the extension. it does not rewrite the path to one with an extension. it resolves the import to a specific file, adding the js extension if there is none, since that is what the most popular resolver (node) does. hence, there is no such "asymmetricity". with regards to not remapping paths - typescript is not primary a compiler. it is a linting tool. it is an explicit non-goal for it to do anything more than check, and strip, types.Overhappy
This and rest of the comments in sequence are Original Side Note Moved as Comments: Regarding the TS team's failure on resolving this issue, it appears that there is a tendency to try to blow up this issue out of context than what it really is and attach that to some design principles to defend.Outstretched
However, factually this is nothing more than an issue with how the compiler deals asymmetrically with the extension. The Typescript compiler allows import statement without an extension. It then goes on to add ".js" extension to the corresponding output filename while the file is being translated, but for the corresponding import statements where this file is referenced it ignores the fact that it has added ".js" extension during translation. How can this asymmetricity be defended by the out of context URI rewriting principles?Outstretched
There is a fixed one to one correspondence between the Typescript file and the generated Javascript output file during compilation. If the referenced import does not exists, the compiler would throw an error. The files wouldn't even compile! So, out of context or non-compilable examples mentioning the possibility of other conflicting URIs invalidate such claims.Outstretched
If the compiler, simply generated extension-less output files it would also solve the issue. But, would that also somehow violate the design principle regarding URI rewrites? Certainly, in that case there could exist other design principles to defend the position! But wouldn't such stubbornness only help to further validate the adamancy or ignorance of the TS team on this issue?Outstretched
Finally, there is no purpose of having to defend this question, because the problem is real and is evident by now. Again, it doesn't matter whether it is a language or design choice or design principle etc., an external optional simple post-compilation tool could have simply fixed this issue. So, to those comments or answers defending the design choice, while your points could be valid, they don't solve the real problem developers were facing and the question didn't intend to argue over design choices. Only goal was to find a possible solution and help novice programmers.Outstretched
it does not add the .js extension to the filename because of the imports. it converts the .ts extension to the .js extension. and as mentioned above, it doesn't convert any path to one that has .js appended. it resolves the path - one way to see it is: note that relative paths are not "real" paths - by themselves, they do not provide enough information to reach the target file, and so converting the relative path to an absolute path is also part of resolution - which would explain why you don't see absolute paths in the compiled files eitherOverhappy
@somebody. As mentioned above the question is not about why or how of the language/compiler/design choices, but the solution to the problem which can help developers and beginners facing the issue. Again, a simple optional post compilation external tool/extension could have easily fixed such issues, so your argument doesn't help those facing the issue.Outstretched
Is there any actual officially recommended way of using transpiled JavaScript in a browser?Dairen
S
39

If you are using VS code, you can use regex to replace the paths.

Find: (\bfrom\s+["']\..*)(["'])

Replace: $1.js$2

This solution is inspired on a previous solution, but this one works better because of the reasons outlined below. Note that this solution is not perfect since it uses regex instead of syntactically analyzing file imports.

Ignores npm imports. Example:

import fs from 'fs'

Supports multi-line imports. Example:

import {
  foo,
  bar
} from './file'

Supports as imports. Example:

import * as foo from './file'

Supports single and double quotes. Example:

import foo from './file'
import foo from "./file"

Supports exports. See export docs. Example:

export { foo } from './file'
Sulfapyridine answered 22/7, 2022 at 5:40 Comment(5)
(from\s+["']\.?\.\/.*)(["']) is slightly better because it matches on ../ and ./, and ignores existing .js importsToxinantitoxin
(from\s+)(["'])(?!.*\.js)(\.?\.\/.*)(["']) doesn't match .js, and gives you capture groups for writing it back with $1$2$3.js$4in VS CodeEugene
@wesbos, I used your suggest regex and shelljs' sed to build a small script that can be used in build scripts, without the need to manually use VS Code substitutions: npmjs.com/package/fix-tsc-es-imports Thank you.Purlieu
THANK YOU! I was searching for this for quite a whileRadu
/(?<from>from\s*)(?<quote>["'])(?<path>(?!.*\.js)\.?\.\/.*?)(?=\k<quote>)/gm doesn't match .js, and gives you named capture groups while ensuring that quotes are the same characters. You can write it back with ${from}${quote}${path}.js${quote}Archon
M
29

you also can add nodejs CLI flags for enable node module resolution:

  • for importing json --experimental-json-modules
  • for importing without extensions --experimental-specifier-resolution=node
node --experimental-specifier-resolution=node dist/some-file.js

It's worth mentioning --experimental-specifier-resolution=node has a bug (or not) then you cannot run bin scripts without extensions (for example in package.json bin section, "tsc" won't work, but "tsc":"tsc.js" will work).

Too many packages have bin scripts without any extensions so there is some trouble with adding NODE_OPTIONS="--experimental-specifier-resolution=node" env variable

Maeda answered 19/1, 2022 at 17:11 Comment(5)
Thanks Andrew, this works for me, I add it as an extra option on the shebang line of my JS bin entry point #!/usr/bin/env node --experimental-specifier-resolution=nodeWardieu
This node flag is the best solution - it ensures you don't have to add the '.js' extension to all imports, it works with nodemon and supervisor AND it works with jest (I ran into a situation where I added the .js extension when running the server, but the .js extension broke my tests when running jest - this node flag solves this). Originally saw this same solution here - https://mcmap.net/q/129882/-how-to-resolve-node-js-es6-esm-modules-with-the-typescript-compiler-tsc-tsc-doesn-39-t-emit-the-correct-file-ext.Landfall
This solution is unfortunately not applicable for node 19. Does someone has a replacement?Bret
There is a module for node 19 called extensionless. github.com/barhun/extensionlessBret
See my answer for a similar solution that also understands TypeScript's compilerOptions.paths configuration.Express
S
18

Another solution would be to use tsc-alias which has a resolveFullPaths option.

Attempt to replace incomplete import paths (those not ending in .js) with fully resolved paths (for ECMAScript Modules compatibility)

I initially installed and set up tsc-alias to resolve some aliased imports (I like to avoid using parent imports but in cases where it is necessary, such as constants or utility functions, I use aliases). Once working I found that the resolveFullPaths option fixed the issue OP described.

  1. Install tsc-alias

    yarn add -D -E tsc-alias
    
  2. Configure tsconfig.js

    {
      "compilerOptions": {
        ...
      },
      "tsc-alias": {
        "resolveFullPaths": true,
        "verbose": false
      }
    }
    
  3. Update your build/compile script to call tsc-alias after tsc

    "scripts": {
        "compile": "tsc && tsc-alias"
    }
    
South answered 13/7, 2023 at 10:17 Comment(0)
M
9

npm, anyone?

npm i fix-esm-import-path

check it on npmjs or github.

Only has 8 stars (one is from me), but I'm using it on multiple projects and it does what I need:

npx fix-esm-import-path dist/your-compiled-entrypoint.js
Malissamalissia answered 6/12, 2022 at 16:7 Comment(0)
P
7

As many have pointed out. The reason why TypeScript doesn't and will never add file extension to import statements is their premise that transpiling pure JavaScript code should output the same JavaScript code.

I think having a flag to make TypeScript enforce file extensions in import statements would be the best they could do. Then linters like ESLint could maybe offer an auto fixer based on that rule.

Pyrrhotite answered 22/4, 2021 at 20:58 Comment(0)
P
5

In case you have trouble with TypeScript and ESM, there is this tiny library that actual works perfectly:

npm install @digitak/tsc-esm --save-dev

Replace the tsc call with tsc-esm in your scripts:

{
   "scripts": {
      "build": "tsc-esm"
   }
}

Finally you can run:

npm run build
Paquito answered 10/3, 2022 at 0:25 Comment(1)
From author: I don't recommend anymore to use this. Since TS 4.7 it is now possible to set the moduleResolution field to node16 or nodeNext which enforces the use of the .js extension at the end of the imports. I first developped tsc-esm as a patch to the tsc compiler that would add the .js extension. But this technique has drawbacks. For example, it cannot deal with Typescript aliases. I strongly encourage to follow the Ecmascript guidelines and the recent Typescript extension, that is to use Typescript@^4.7 and to set the moduleResolution field to node16 or nodeNext in tsconfig.json.Heterogenetic
P
3

tsc-esm-fix can help:

npm install --save-dev tsc-esm-fix

# to post-process outputs each time
npx tsc-esm-fix --target='target/es6'

# to patch ts sources once
npx tsc-esm-fix --src='src/' --ext='.js'

example diff:

-import { foo } from './foo';
+import { foo } from './foo.js';

-import * as e1def from 'e1/a/b/c';
+import * as e1def from 'e1/a/b/c/index.js';
Prig answered 11/8, 2023 at 8:44 Comment(0)
A
1

(Probably) Better Answer

See here for a potentially better answer based on this idea & proposed in the comments.

I haven't tested this yet, but seems like my original answer below is lacking & seems like the linked answer is better. I can't say for sure but I'd recommend people check that out first.

My Original Answer

If you know that all your import statements should really have the .js extension, and all imports either have no extension or already have the .js extension, you could use a regex find/replace to "normalise" everything. I would advise you just check your git (or other VCS) logs before committing the change. Here are the regexes I use in VSCode:

  • Find: (import .* from ".*(?!\.js)(.){3})".
  • Replace: $1.js".

The find expression will match imports without the .js extension. The first group will capture the part up to the closing quote. The replace expression then takes the first group of the match, which always doesn't have the .js extension, and then appends the extension.

Failing getting a linter set up, you could run this periodically & check the git logs to ensure no imports without the extension slip into the codebase.

Aerostatics answered 14/7, 2022 at 17:39 Comment(3)
The regex above will add ".js" to absolute imports (e.g. import X from "libX");` you might need to add in a check for a leading "." (e.g. (import .* from "\..*(?!\.js)(.){3})"Struthious
You need to add the final " back. In addition to @Mattia's comment, this doesn't work with single quotes, doesn't handle export paths, doesn't work with multi-line imports and doesn't work if you import a module as something else. However, your solution inspired me to write a slightly better solution below.Sulfapyridine
@Sulfapyridine I haven't tested your work but all of your points seem valid and at a quick glance it looks like you've successfully built upon my original idea and improved it. Thanks, and pleasure collaborating with you!Aerostatics
S
1

Had the same issue on a big monorepo, can't edit each file manually, so I wrote a script to fix all esm import in my project and append .js or /index.js in a safe way:

fix-esm-import-paths

Test before using in your project.

Subjoin answered 21/7, 2022 at 12:1 Comment(1)
You should use your script as a node custom loader. See for instance the project of this person which is similar to your script: github.com/barhun/extensionlessBret
C
0

For the current day, I would recommend end your imports always with .js in your source even if they are .ts files. Typescript is okay with that. Therefore you are going to have .js imports in your build.

Example project:

project/src/
    index.ts
    mod.ts

index.ts:

import {something} from 'mod.js'

something()

When you build:

project/build/
    index.js
    mod.js

you will have all native es imports that ends with js and your IDEs and compilers won't complain about it.

Classmate answered 5/4, 2023 at 11:36 Comment(0)
N
0

If you're using vscode and just want to have the imports with their extensions take a look at this issue.
It doesn't fix the problem with TS compiler but makes vscode not to shorten the import paths.

Nosography answered 23/5, 2023 at 12:58 Comment(0)
E
0

I wrote a custom Node.js ESM loader to solve this exact issue.

https://github.com/olalonde/tsc-module-loader

npm install --save tsc-module-loader
node --experimental-loader tsc-module-loader build/some-file.js

Alternatively, you can register the loader programmatically in your code.

The idea is to have the loader behave exactly like TypeScript's module resolution does, so it doesn't require any configuration or extra build step. For example, it understands the compilationOptions.paths option in tsconfig.json.

Express answered 10/9, 2023 at 16:13 Comment(1)
I like your idea of using a loader. Does this also add extensions to relative paths without any extensions? Looking at the source code it looks like there must be a .ts or .d.ts extension.Mablemabry
B
0

Here is my workaround to replace local imports and exports:

package.json

"scripts": {
    "build:fix:esm": "sh esm_fix.sh",
    "prepub": "cp ./package.module.json ./dist/esm/package.json"
}

esm_fix.sh

#!/bin/sh

if [[ "$OSTYPE" == "darwin"* ]]; then
  find ./dist/esm -name "*.js" -exec sed -i '' -E "s#export (.*) from '\.(.*)';#export \1 from '.\2\.js';#g" {} +;
  find ./dist/esm -name "*.js" -exec sed -i '' -E "s#import (.*) from '\.(.*)';#import \1 from '.\2\.js';#g" {} +;
else
  find ./dist/esm -name "*.js" -exec sed -i -E "s#export (.*) from '\.(.*)';#export \1 from '.\2\.js';#g" {} +;
  find ./dist/esm -name "*.js" -exec sed -i -E "s#import (.*) from '\.(.*)';#import \1 from '.\2\.js';#g" {} +;
fi

package.module.json

{
  "type": "module"
}

See working package.json example.


Note! When you are using aliases for your local packages, you will need to adjust esm_fix.sh file accordingly!

Example: import { Cache } from '@services/cache'; Regular expression will look like:

-E "s#export (.*) from '\@services(.*)';#export \1 from '@services\2\.js';#g"
-E "s#import (.*) from '\@services(.*)';#import \1 from '@services\2\.js';#g" {} +;
Bribe answered 13/11, 2023 at 7:25 Comment(2)
will this work if i am using path aliases?Coad
for path aliases you will need to adjust esm_fix.sh file accordingly to your aliases (add extra replacing lines for each alias). the example was in the answer, under the line.Bribe
A
0

For Node.js 19+ users i made a lib specifier-resolution-node solving imports. Install it then run node with this argument

node --import=specifier-resolution-node/register index.js
Alveolate answered 7/12, 2023 at 10:30 Comment(0)
M
0

I wrote @knighted/specifier because TypeScript continues to refuse to rewrite specifiers during compilation and wanted to create dual ESM and CJS builds with tsc.

You could use it before or after compilation to add extensions to relative imports.

For example, update all relative specifiers that do not end in .js to end in .js for file dist/index.js:

import { writeFile } from 'node:fs/promises'
import { specifier } from '@knighted/specifier'

// `value` is the specifier value. `return` the new value.
const { code, error } = await specifier.update('dist/index.js', ({ value }) => {
  const relative = /^(?:\.|\.\.)\//

  if (relative.test(value) && !value.endsWith('.js')) {
    return value.replace(/(.+)/, '$1.js')
  }
})

if (code && !error) {
  await writeFile('dist/index.js', code)
}

If there are no errors you can overwrite the original file with the updated code.

Mablemabry answered 12/1 at 1:36 Comment(0)
A
0

I personally think that the best solution is doing post-build processing. I see that a few npm packages seem to solve this problem but I was a bit concerned by some of the regular expressions I saw in the answer and some comments. I did my own version which has only Node.js native dependencies and that is pretty lightweight.

This can be added as an extra step after running your tsc command (you would run the compiled .js file). Here is the TypeScript code for it:

import { existsSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs'
import { EOL } from 'node:os'
import { dirname, join, relative, resolve } from 'node:path'

const esmFileExtensionRegExp =
  /(?<from>from\s*)(?<quote>["'])(?<path>(?!.*\.js)(\.|\.?\.\/.*?)\.?)(\k<quote>)/gm

/**
 * Get all ESM file paths (`.js` and `.d.ts`) from a directory.
 *
 * @param esmBuildDirectoryPath - The path to the ESM build directory.
 *
 * @returns An array of ESM file paths.
 */
const getEsmFilePaths = (esmBuildDirectoryPath: string): string[] =>
  readdirSync(esmBuildDirectoryPath, { withFileTypes: true }).reduce<string[]>((files, entry) => {
    const absoluteEntryPath = resolve(esmBuildDirectoryPath, entry.name)
    const relativeEntryPath = relative(process.cwd(), absoluteEntryPath)
    if (entry.isDirectory()) {
      return [...files, ...getEsmFilePaths(absoluteEntryPath)]
    } else if (entry.isFile() && /\.(d\.ts|js)$/.test(absoluteEntryPath)) {
      return [...files, relativeEntryPath]
    }
    return files
  }, [])

console.log(`${EOL}🏃 Running build step: add ESM file extensions.${EOL}`)

getEsmFilePaths('lib/esm').forEach((filePath) => {
  const fileContent = readFileSync(filePath).toString()
  const newFileContent = fileContent.replace(
    esmFileExtensionRegExp,
    (_match, from: string, quote: string, path: string) => {
      const fromPath = resolve(join(dirname(filePath), path))

      // If the path exists without any extensions then it should be a directory.
      const fromPathIsDirectory = existsSync(fromPath)

      if (fromPathIsDirectory && !statSync(fromPath).isDirectory()) {
        throw new Error(`🚨 Expected ${fromPathIsDirectory} to be a directory`)
      }

      // Add the missing extension or `/index` to the path to make it ESM compatible.
      const esmPath = fromPathIsDirectory ? `${fromPath}/index.js` : `${fromPath}.js`

      if (!existsSync(esmPath)) {
        throw new Error(`🚨 File not found: ${esmPath}`)
      }

      if (!statSync(esmPath).isFile()) {
        throw new Error(`🚨 Expected ${fromPathIsDirectory} to be a file`)
      }

      const newPath = `${path}${fromPathIsDirectory ? '/index' : ''}.js`
      console.log(`   ➕ ${filePath}: replacing "${path}" by "${newPath}"`)
      return `${from}${quote}${newPath}${quote}`
    }
  )

  writeFileSync(filePath, newFileContent)
})

In my packages, I have dual build output for both CommonJS and ESM in lib/cjs and lib/esm. I use 2 different tsconfig files to let TypeScript handle the builds and simply run that script after, on the ESM build.

You can see a fully working example here.

Archon answered 3/3 at 2:29 Comment(0)
A
-1

I usually just use the .js extension in import statements in typescript files as well and it works.

Not using a file extension in import paths is a nodejs only thing. Since you are not using commonjs but module you are not using nodejs. Therefore you have to use the .is extension in import paths.

Antagonist answered 28/6, 2020 at 7:7 Comment(0)
D
-3

TypeScript cannot possibly know what URI you are going to use to serve your files, therefore it simply must trust that the module path you gave it is correct. In this case, you gave it a path to a URI that doesn't exist, but TypeScript cannot know that, so there is nothing it can do about it.

If you are serving the module with a URI that ends in .js, then your module path needs to end in .js. If your module path doesn't end in .js, then you need to serve it up at a URI that does not end in .js.

Note that the W3C strongly advises against using file extensions in URIs, because it makes it harder to evolve your system, and advocates to instead rely on Content Negotiation.

Rewriting paths would break a couple of fundamental design principles of TypeScript. One design principle is that TypeScript is a proper superset of ECMAScript and every valid ECMAScript program and module is a semantically equivalent TypeScript program and module. Rewriting paths would break that principle, because a piece of ECMAScript would behave differently depending on whether it is executed as ECMAScript or TypeScript. Imagine, you have the following code:

./hello

export default "ECMAScript";

./hello.js

export default "TypeScript";

./main

import Hello from "./hello";

console.log(Hello);

If TypeScript did what you suggest, this would print two different things depending on whether you execute it as ECMAScript or as TypeScript, but the TypeScript design principles say that TypeScript does never change the meaning of ECMAScript. When I execute a piece of ECMAScript as TypeScript, it should behave exactly as it does when I execute it as ECMAScript.

Daydream answered 28/6, 2020 at 7:36 Comment(7)
That example is way far-fetched and the design principle being used to defend that is quite out of context and irrelevant for the actual issue where one-to-one correspondence must exist between the js and ts files.Outstretched
@user3330840: A compiler cannot pick and choose which parts of a language specification it supports. Anything which is allowed by the spec will be written by a user. In fact, in my current project I crucially depend on the fact that TypeScript leaves the module paths alone, because I allow the end user to supply API-compatible libraries in multiple different languages (TypeScript, ECMAScript, CoffeeScript, and JSON), and the module loader to pick up the correct file extension.Linden
It is a shame that this has been downvoted because it is the correct answer. The way TypeScript behaves here is certainly annoying in some common situations but unfortunately the correct answer isn't always the same as the answer we wish was correct.Mcconaghy
@DanielCassidy: As I mentioned in my comment above, that "far-fetched example that nobody would ever write" is actual working code from a project of mine. That project depends on the fact that TypeScript loads exactly the module I tell it to, and doesn't muck around with paths. I hope all viewers who come to this question will notice that the accepted answer, which is really a rant disguised as an answer, happens to be written by the question asker. Personally, I don't really get what's so hard to understand: if you ask TypeScript for a module named foo, it will compile that to code …Linden
… which loads a module named foo. If you ask TypeScript for a module named bar, it will compile that to code which loads a module named bar. If you ask TypeScript for a module named bar.js, it will compile that to code which loads a module named bar.js. Why would it do anything else?Linden
@JörgWMittag That's what I said. It's a shame your answer was downvoted.Mcconaghy
@JörgWMittag: What does it mean for code to execute "as ECMAScript or as TypeScript"? I'm not trying to be pedantic just to understand. Does that mean when you execute code that was compiled from an ECMAScript source vs a Typescript source?Struthious
N
-7

You can use the same solution as me

File: tsconfig.json

"compilerOptions": {
    "module": "commonjs",   ==> not required extension when import   
    "target": "ES6",
},

Because use commonjs, you must remove "type": "module" in package.json

Done :D

Nazarene answered 20/2, 2022 at 14:55 Comment(1)
This doesn't really do the trick. Some modules like got or node-fetch in newer versions require to use ESM modules, and don't work with CommonJS.Alary

© 2022 - 2024 — McMap. All rights reserved.