javascripttypescripttsctranspilerdynamic-import

Transpile dynamic import with tsc?


I’ve been working with Typescript for some time. We use localization in our frontend web components and we have our own implementation for it.

So, we have a translations folder where we have all the locale files with translations as key value pairs.

We use the locale files to load the translation files based on the language selected by the user from the browser.

And we load the locales like this with a dynamic import:

{
  'component': locale => import(`./translations/${locale}.ts`),
},

While everything works fine when we use the extension .js but when we use .ts, tsc wasn’t able to transpile this part of the code.

I would expect the build to be something like this:

{
  'component': locale => import(`./translations/${locale}.js`),
},

Does anybody has a clue why that is?


Solution

  • TLDR;

    It's not possible to use .ts extension and have the Typescript compiler change that to .js, as the Typescript compiler itself never modifies module specifiers (that is the path of a module). So, you'll have to provide a .js extension if you're using tsc, else, if you really need to use it, you'll need to have some bundler like Webpack or Vite bundle your code, and then transpile it to javascript.

    ESM in Typescript

    ESM (ECMAScript Modules) available in Node version greater than 12.0.0, requires one to specify the extension while importing an ESM module. The same is the case if you use them in a browser. So, we need to add a .js extension.

    The Typescript documentation for ESM modules itself specifies to make use of a .js extension to get correct runtime file as a rule for using ESM imports. To quote from the Typescript ESM docs -

    relative import paths need full extensions (e.g we have to write import ./foo.js instead of import ./foo)

    And the reason it is the case, is because Typescript compiler never changes the module specifier. To quote from the Typescript ESM docs -

    When a .ts file is compiled as an ES module, ECMAScript import/export syntax is left alone in the .js output

    Which is the reason why it's not changed when you compile it.

    Status Quo

    There have been many issues created on GitHub for problems that arise out of this and asking for support -

    Until there was this proposal and the option moduleResolution was updated with a new option bundler with this PR, which explicitly specifies -

    Who should use this mode?


    • ✅ Application authors who use a bundler on their TS or JS files before a runtime consumes that bundle
    • ✅ Application authors who run in Bun
    • ✅ Library authors who use a bundler to deploy a UMD bundle
    • ⚠️ Library authors who use a tool like Rollup to deploy multiple builds in different module formats—defer to advice from your build tool
    • 🚫 Anyone intending to produce modules with tsc that will run in Node or the browser without further bundling or processing
    • 🚫 Anyone intending to produce modules with tsc that will run in Deno without further bundling or processing

    Why? Because Typescript compiler itself never modifies the module specifiers, and one needs to be dependent on an external bundler to do that for you.

    What can you do?

    I'd say just use the .js extension, unless you have a very good reason to not to use it (which I doubt there would be one). Otherwise, add the moduleResolution with bundler option and noEmit with true values in your tsconfig.json, along with allowImportingTsExtensions option to use the .ts extension and configure a bundler to use with it.