-
Section 1: Getting Started
-
Section 2: Basic Types
-
Section 3: Functions and Objects
-
Section 4: Advanced Types
-
Section 5: Object-Oriented TypeScript
-
Section 6: Working with Modules
-
Section 7: Tooling and Practice
-
Section 8: Type-Level Programming
-
Section 9: TypeScript with Backends
-
Section 10: Testing Typed Code
-
Section 11: Data Validation with Types
-
Section 12: Practical Projects
-
Section 13: Compiler Internals
-
Section 14: Configuration Deep Dive
-
Section 15: Enums, Symbols, and Special Types
-
Section 16: Working with Async Code
-
Section 17: TypeScript and the DOM
-
Section 18: Advanced Generics Practice
-
Section 19: Working with Third-Party Types
-
Section 20: Monorepo and Large-Scale Practices
-
Section 21: Common Pitfalls and Best Practices
-
Section 22: Interview Practice
-
Section 23: Handbook: Narrowing In Depth
-
Section 24: Handbook: Object Types In Depth
-
Section 25: Handbook: Classes In Depth
-
Section 26: Handbook: Modules In Depth
-
Section 27: Handbook: Declaration Files In Depth
-
Section 28: JSX and Namespaces
-
Section 29: Compiler Configuration Reference
-
Section 30: More Practice Exercises
-
Section 31: Handbook: Everyday Types Deep Dive
-
Section 32: Utility Types Full Reference
-
Section 33: Decorators Reference
-
Section 34: Mixins and Advanced OOP Patterns
-
Section 35: Iterators and Generators Typing
-
Section 36: More Type-Level Programming Practice
-
Section 37: TypeScript Ecosystem Tools
-
Section 38: TypeScript with Testing Frameworks
-
Section 39: TypeScript for Library Authors
-
Section 40: More Interview Practice
-
Section 41: Handbook: Functions In Depth
-
Section 42: Handbook: Type Manipulation Deep Dive
-
Section 43: More Real-World Patterns
-
Section 44: TypeScript Release Notes Highlights
-
Section 45: Final Practice and Review
158: Module Resolution: Node vs Bundler vs Classic
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_modulesfolder) and check the inventory list (thepackage.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
Node16orNodeNext. If you're building a frontend app with Vite or Next.js, usebundler. If you're usingclassic, 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.
There are no comments for now.