TypeScript: "Module not found" when extending a class otherwise same code without extending no error

Bug Report

Every time when I try to extend the abtract class Command and I try to compile I get the error: Error: Cannot find module src/classes/commandBase

🔎 Search Terms

  • Module not found
  • class module not found

🕗 Version & Regression Information

  • v4.8.4

💻 Code

My project has the following structure:

   ├────tsconfig.json
   └────src
        ├───classes
        │   └───commandBase.ts
        ├───commands
        │   └───ping.ts
        ├...

tsconfig.json

{
  "compilerOptions": {
    "target": "ESNext",
    "module": "CommonJS",
    "rootDir": "./src/",
    "outDir": "./dist/",
    "strict": true,
    "moduleResolution": "node",
    "importHelpers": true,
    "experimentalDecorators": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "allowSyntheticDefaultImports": true,
    "resolveJsonModule": true,
    "forceConsistentCasingInFileNames": true,
    "removeComments": true,
    "typeRoots": ["node_modules/@types"],
    "sourceMap": true,
    "baseUrl": "./"
  },
  "include": ["./**/**/*.ts"],
  "exclude": ["node_modules", "dist"]
}

commandBase.ts

abstract class Command {
  name: string;
  abstract execute(client: Client, message: Message, args: string[]): void;

  constructor(name: string) {
    this.name = name;
  }
}
export default Command;

ping.ts

import Command from "src/classes/commandBase";

class Ping extends Command {
  constructor() {
    super("ping");
  }
  async execute(
    client: Client<boolean>,
    message: Message<boolean>,
    args: string[]
  ): Promise<void> {
    const m = await message.channel.send("Ping");
    m.edit(
      `Pong! Latency is ${
        m.createdTimestamp - message.createdTimestamp
      }ms. Discord Chat API Latency is ${Math.round(client.ws.ping)}ms`
    );
  }
}

export default Ping;

🙁 Actual behavior

Error: Cannot find module 'src/classes/commandBase'.

However if you change import Command from "src/classes/commandBase"; to import Command from "../classes/commandBase"; everything is working fine when importing.

BUT the weird behavior starts now: If you create a new file which import Command from 'src/classes/commandBase and exports a variable everything is working as intended. The error only appears when you extend a class.

file foo.ts

// This is working 
import Command from "src/classes/commandBase";

const foo: Command = {
  name: "",
  execute: function (
    client: Client,
    message: Message,
    args: string[]
  ): void {
    throw new Error("Function not implemented.");
  },
};
export default foo;

🙂 Expected behavior

Since the module exists it should be able to import.

About this issue

  • Original URL
  • State: closed
  • Created 2 years ago
  • Comments: 16 (5 by maintainers)

Most upvoted comments

// This is working 
import Command from "src/classes/commandBase";

const foo: Command = {

This is because Command is only used in a type position, so the import is removed from the JS.

This thread is a monument to all our past bad decisions around modules and imports 🥲

If you get tired of imports always looking like “…/” or “./”, or needing to change them as you move files, this is a great way to fix that.

This text is one of my biggest pet peeves in the documentation. It’s described as if it’s some convenience feature that TS will take care of for you, but in reality it only works if your runtime environment also understands it, because TS emits the import paths you use verbatim.

Autocompletion is done by the TS language server, so this is almost certainly a case where TS understands your imports but your runtime (assumedly node) doesn’t, and your configuration should be adjusted to align. What I said above still applies, at any rate.

It is weird that the console.log example works, though.

As a general rule, in TS you need to use the import path that will work at runtime (i.e. the exact string you would pass to require() or use for an import statement in a plain .js file). Anything else may compile but will then fail at runtime.

PS D:\TypeScript-coding\05-Inheritance> node driver.ts (node:14380) Warning: To load an ES module, set “type”: “module” in the package.json or use the .mjs extension. (Use node --trace-warnings ... to show where the warning was created) D:\TypeScript-coding\05-Inheritance\driver.ts:1 import { Shape } from “./shape”;

Is this a runtime error, or a compile-time error? I assume it’s the former, in which case the error goes away in the second example because you only use the type so TS elides the import. You’re using moduleResolution node, but src/classes/commandBase generally won’t work as an import specifier in node without specific configuration. You need to use a relative path, i.e. one starting with ./ or ../.