I am having a problem with importing ESM modules in my project based on Nest.js. As far as I understand, this problem is relevant not just to Nest.js but typescript as well.
I have tried various things and combinations of Node.js & typescript versions, adding "type":"module"
to package.json
& changes in the settings of my tsconfig.json
file, so it has the following view, which is far from default values:
{
"compilerOptions": {
"lib": ["ES2020"],
"esModuleInterop": true,
"module": "NodeNext",
"declaration": true,
"removeComments": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"moduleResolution": "Node",
"target": "esnext",
"sourceMap": true,
"outDir": "./dist",
"baseUrl": "./",
"incremental": true,
"skipLibCheck": true,
"strictNullChecks": false,
"noImplicitAny": false,
"strictBindCallApply": false,
"forceConsistentCasingInFileNames": false,
"noFallthroughCasesInSwitch": false,
}
}
My full environment is:
But it still gives me an error when I am trying to import any ESM module in any of my services. For example:
import random from `random`;
export class AppService implements OnApplicationBootstrap {
async test() {
const r = random.int(1, 5);
console.log(r);
}
}
Does anyone have a clue how to fix it?
This Problem seems to occur more frequently since more packages are switching over to be distributed as ES module.
Summary
There are two approaches for this problem.
import()
functionThe instruction to use import()
for ES modules in CommonJS can be found everywhere. But when using typescript the additional hint is missing how to prevent the compiler to transform the import()
call to a require()
. I found two options for this:
moduleResolution
to nodenext
or node16
in your tsconfig.json
(Variant 1)eval
workaround - this is based on the section "Solution 2: Workaround using eval" from answer https://stackoverflow.com/a/70546326/13839825 (Variant 2 & 3)await import()
whenever neededThis solution is often suggested on the official NestJS Discord
"moduleResolution": "nodenext"
or "moduleResolution": "node16"
to your tsconfig.json
await import()
call when neededasync
contextimport { Injectable } from '@nestjs/common';
@Injectable()
export class AppService {
async getHello(): Promise<string> {
const random = (await import('random')).default;
return 'Hello World! ' + random.int(1, 10);
}
}
Note: Importing a type from a ES module will not result in an require
call, since it's only used by the typescript compiler and not the runtime environment.
async
contextimport { Injectable } from '@nestjs/common';
import { type Random } from 'random';
async function getRandom(): Promise<Random> {
const module = await (eval(`import('random')`) as Promise<any>);
return module.default;
}
@Injectable()
export class AppService {
async getHello(): Promise<string> {
return 'Hello World! ' + (await getRandom()).int(1, 10);
}
}
import { Injectable } from '@nestjs/common';
import { type Random } from 'random';
let random: Random;
eval(`import('random')`).then((module) => {
random = module.default;
});
@Injectable()
export class AppService {
async getHello(): Promise<string> {
return 'Hello World! ' + random.int(1, 10);
}
}
Although not supported, it seems possible to setup NestJS to compile to ESM. This Guide has good instructions how to do this for any typescript project.
I tested it with NestJS and found these steps sufficient:
"type": "module"
to your package.json
module
to NodeNext
in your compilerOptions in tsconfig.json
.js
extension to all of your relative importsNow the import should work as expected.
import { Injectable } from '@nestjs/common';
import random from 'random';
@Injectable()
export class AppService {
async getHello(): Promise<string> {
return 'Hello World! ' + random.int(1, 10);
}
}
What did not work so far were the unit tests with jest. I got other import errors there and I bet there are more problems down the road. I would avoid this approach and wait until NestJS officially supports ES modules.