Skip to Content
Course content

158: Module Resolution: Node vs Bundler vs Classic

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Imagine you're trying to find a specific tool in a massive workshop. Depending on who you ask for directions, the "system" for finding that tool changes completely.

  • The Classic Way: You only look at the workbench right in front of you. If the tool isn't sitting there, you assume it doesn't exist.
  • The Node Way: You check the workbench first. If it's not there, you go to the main tool chest (the node_modules folder) and check the inventory list (the package.json) to see where it's stored.
  • The Bundler Way: You have a personal assistant (like Vite or Webpack). You just tell them "get me the hammer," and you don't care where it is or how it's labeled; the assistant knows the layout of the whole shop and just hands it to you.

In TypeScript, moduleResolution is exactly that: it's the set of rules the compiler uses to figure out which file on your hard drive corresponds to the string you wrote in an import statement.

The relic known as "Classic"

You'll almost never use moduleResolution: "classic" in a modern project, but you might see it in ancient repos. It's incredibly simplistic. If you import ./utils, it looks for utils.ts. If that's not there, it looks for utils.js. It doesn't know what node_modules is. It's essentially the "workbench" approach. If you're starting a project today, stay far away from this.

Following the Node.js trail

When you set moduleResolution: "node" (which is technically Node 10), you're telling TypeScript to mimic how the old CommonJS version of Node.js works. This is where the "inventory list" comes in. When you write import { formatDate } from "./utils", TypeScript looks for the file. But when you write import axios from "axios", it knows it shouldn't look in your local folder. It jumps straight to node_modules/axios, looks for a package.json, and reads the "main" field to find the actual entry point.

I've spent way too many hours debugging "Module not found" errors because I forgot that node resolution relies heavily on those package.json entry points.

The "Bundler" and the ESM headache

Here is where things get spicy. With the rise of ES Modules (ESM), Node.js started demanding that imports include file extensions. Suddenly, you're forced to write import { helper } from "./helper.js"—even though the file on your disk is actually helper.ts. It feels wrong, right? It feels like a lie.

If you're using a tool like Vite, Webpack, or Esbuild, those tools are smart enough to resolve the files without the extensions. To stop TypeScript from complaining that your "lie" is a mistake, TypeScript 5.0 introduced moduleResolution: "bundler".

// tsconfig.json
{
  "compilerOptions": {
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "noEmit": true 
  }
}

When you use "bundler", you're essentially telling TypeScript: "Stop worrying about the strict Node.js rules. I have a bundler handling the actual file linking, so just let me import things the way the bundler likes it."

Which one should you actually pick?

If I'm mentoring you on a new project, here's my rule of thumb: If you're building a library that needs to run directly in Node.js, use Node16 or NodeNext. If you're building a frontend app with Vite or Next.js, use bundler. If you're using classic, please tell me so I can help you migrate your project to the current decade.




📋 Practical Task

Fixing the "Missing Extension" Conflict in a Vite Project

You have a small project using Vite and TypeScript. You are trying to import a utility function, but TypeScript is throwing an error because it's currently configured with moduleResolution: "node", and it's confused by the lack of file extensions in your imports (or it's demanding .js extensions that you don't want to write).

Your Goal: Modify the tsconfig.json to allow the project to resolve modules using a modern bundler strategy, removing the need for explicit .js extensions in your TypeScript imports.

// current tsconfig.json
{
  "compilerOptions": {
    "target": "ESNext",
    "module": "ESNext",
    "moduleResolution": "node", // This is the problem!
    "strict": true,
    "esModuleInterop": true
  }
}

// src/main.ts
import { calculateTotal } from "./cart-utils"; // Error: Cannot find module './cart-utils' or its corresponding type declarations.

Instructions: Update the moduleResolution setting in the tsconfig.json and add the necessary noEmit flag (since the bundler, not tsc, handles the output) to make the error in main.ts disappear.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.