Search code examples
node.jstypescriptes6-modules

Force TypeScript to generate export/imports with the ".js" extension; running Node 16?


Have a TS 4.7 library using ESM modules.

tsconfig.json:

    "target": "ES2020",
    "module": "ES2020",
    "lib": ["ES2020"], 
    "moduleResolution": "node",

package.json

"type": "module",

I have a main file with only a one silly export:

index.ts

export { Spig } from './spig';

which is compiled to:

index.js

export { Spig } from './spig';
//# sourceMappingURL=index.js.map

Problem

When I use this library from a Node CLI program (with ESM modules enabled as well), I get the following error:

Cannot find module <path>/lib/spig imported from <path>/lib/index.js

When I manually add .js in the generated index.js, the issue is gone:

export { Spig } from './spig.js';

How can I force TypeScript compiler to generate the extension, too? What am I missing here?


Solution

  • You cannot omit the file extension anymore in ESM module imports. The extension should be always .js/.jsx, not .ts/.tsx for a typescript file. So, in the index.ts you should add the extension to spig export like the following and every other file imported/exported if using ESM modules.:

    index.ts

    export { Spig } from './spig.js'; 
    

    Also, moduleResolution should be set to Node16 or NodeNext so ESM modules work as expected.

    As stated in the docs (enphasis by me):

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

    When a .ts file is compiled as an ES module, ECMAScript import/export syntax is left alone in the .js output; when it’s compiled as a CommonJS module, it will produce the same output you get today under module: commonjs.

    This also means paths resolve differently between .ts files that are ES modules and ones that are CJS modules. For example, let’s say you have the following code today:

    // ./foo.ts
    export function helper() {
        // ...
    }
    
    // ./bar.ts
    import { helper } from "./foo"; // only works in CJS
    helper();
    

    This code works in CommonJS modules, but will fail in ES modules because relative import paths need to use extensions. As a result, it will have to be rewritten to use the extension of the output of foo.ts - so bar.ts will instead have to import from ./foo.js.

    // ./bar.ts
    import { helper } from "./foo.js"; // works in ESM & CJS
    helper();
    

    This might feel a bit cumbersome at first, but TypeScript tooling like auto-imports and path completion will typically just do this for you.