node.jstypescripttsctsconfig-pathstypescript-module-resolution

Typescript compiles successfully but node cannot find module


Node error Error: Cannot find module 'hello' even though Typescript compiled successfully.

Dir structure (note this is almost identical to tsconfig docs for baseUrl)

main
| src
| ├─ index.ts
| └─ hello
|      └── index.ts
└── tsconfig.json
└── package.json

I want to only use non-relative imports (so no /, ./, ../). I've encountered this problem in the past and have always resorted to using relative imports, but I do not want to continue doing this.

In the top level index.ts there is import { helloWorld } from 'hello'.

tsc compiles successfully and the output dir dist looks like this:

main
├─ src
├─ tsconfig.json
├─ package.json
└─ dist
    ├─ index.js
    └─ hello
        └── index.js

Running node dist/index.js outputs the error Error: Cannot find module 'hello'. The import statement gets compiled in dist/index.js to require("hello") which is where the error is thrown.

From this answer, it seems like require() will only look at modules in the current directory if the path begins with ./. So when I change it to require("./hello") it works completely fine!

I've been playing around with baseUrl and paths in tsconfig.json but I cannot get it to output require("./hello"). Would really appreciate some help on this issue and to finally get to the bottom of it, thanks!

// tsconfig.json
{
  "compilerOptions": {
    // from extending off "@tsconfig/node16/tsconfig.json"
    "lib": ["es2021"],
    "module": "commonjs",
    "target": "es2021",

    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    // end of @tsconfig/node16/tsconfig.json

    "baseUrl": "src",
    "outDir": "dist"
  },
  "include": ["src"],
  "exclude": ["node_modules"]
}


Solution

  • I found that this is topic has received a lot of attention in Typescript's Github issues. A few of many issues raised about this very problem:

    From the TS Lead himself:

    Our general take on this is that you should write the import path that works at runtime, and set your TS flags to satisfy the compiler's module resolution step, rather than writing the import that works out-of-the-box for TS and then trying to have some other step "fix" the paths to what works at runtime.

    So it seems like we either use relative imports, or use some tool like tsc-alias.