Introduction
Over the past 58 days, we've covered a lot of ground in TypeScript configuration, but there's one setting that trips up more developers than I'd like to admit: moduleResolution: bundler. This setting tells TypeScript how to resolve your imports the way your bundler does, not the way Node.js does—and getting it right can save you hours of debugging mysterious "module not found" errors.
If you're building with Vite or Next.js, this is the setting you need to understand.
What moduleResolution Does
Before we talk about bundler specifically, let's step back. The moduleResolution option in your tsconfig.json tells TypeScript how to find and resolve module imports at compile time. It's essentially saying: "When I see import x from 'something', how should I go looking for that file?"
The most common values are:
node: Follows Node.js's algorithm (traditional CommonJS/ESM lookup)bundler: Follows how bundlers like Webpack, Vite, and Next.js actually resolve modulesnodeNext: Node.js's newer algorithm, useful for libraries
Here's the practical difference: Node.js and bundlers don't always resolve the same way. A bundler might apply custom loaders, handle file extensions differently, or respect export conditions you've defined. If your moduleResolution setting doesn't match what your bundler actually does, TypeScript will give you false negatives—code that compiles but fails at runtime—or false positives—code that TypeScript rejects but your bundler would handle fine.
Matches How Bundlers Resolve Imports
When you set moduleResolution: bundler, you're telling TypeScript: "Resolve imports the way Vite, Webpack, or Next.js would, not the way Node.js would." This is critical because bundlers have their own resolution algorithms that are often more flexible than Node's.
Let me show you a real scenario. Suppose your project structure looks like this:
src/
utils/
helpers.ts
App.tsxIn your App.tsx, you might write:
import { formatDate } from '@/utils/helpers';That @ alias? It's not something Node.js understands natively. But Vite and Next.js do, because they read your tsconfig.json paths and apply them during bundling. If you use moduleResolution: node, TypeScript won't know how to resolve @/utils/helpers, and you'll get a compile error even though your bundler would handle it perfectly.
With Now when TypeScript sees Modern npm packages define their public API using the This says: "You can import from the root or from Here's the catch: I learned this the hard way on a recent project. We had a monorepo where an internal utility was leaked through the If you're consuming packages that use conditional exports—like packages with both ESM and CommonJS versions, or browser/Node variants— Here's where I need to be clear about scope: moduleResolution: bundler, TypeScript respects path aliases, which bundlers honor. Here's the relevant part of a typical tsconfig.json
instead.{
"compilerOptions": {
"moduleResolution": "bundler",
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"@components/*": ["src/components/*"],
"@hooks/*": ["src/hooks/*"]
}
}
}import x from '@/utils/helpers', it knows to look in src/utils/helpers. Your bundler will do the same thing at runtime. They're in sync.Supports Package Exports
exports field in package.json. This is how maintainers control what you can import from a package. For example:{
"name": "my-library",
"exports": {
".": "./dist/index.js",
"./utils": "./dist/utils.js",
"./internal/*": null
}
}
./utils, but you cannot import anything from ./internal." If someone tries import x from 'my-library/internal/secret', it should fail.moduleResolution: node doesn't fully respect exports in all cases, especially with conditional exports (ESM vs CommonJS). moduleResolution: bundler does. Bundlers understand and enforce the exports field because that's how they determine what's public.exports field accidentally. With moduleResolution: node, TypeScript didn't catch it. With bundler, it did—preventing a runtime error months later.bundler is more reliable. Your bundler respects these conditions, and so should TypeScript.Use NodeNext for Libraries Targeting Node
moduleResolution: bundler is for applicationsmoduleResolution: nodeNext
Why the distinction? Because when your library is published to npm and consumed by others, they might be running it on Node.js without a bundler. Or they might use it in both Node and browser environments. nodeNext aligns with Node.js's actual resolution algorithm, so your library works correctly in Node environments.
Here's a practical difference:
// library/tsconfig.json
{
"compilerOptions": {
"moduleResolution": "nodeNext",
"module": "esnext",
"target": "es2020"
}
}
// This library is published and consumed like:
// import { parse } from 'my-library';
// node consumer.mjs // Works correctly because moduleResolution: nodeNextFor an application—a Vite app, a Next.js project, a React Native app—you want bundler. The bundler is responsible for getting code to users, so TypeScript should validate against the bundler's resolution logic.
The heuristic I use: If your build output is a dist/ folder with bundled JavaScript, use bundler. If your build output is transpiled source files meant to be required or imported directly, use nodeNext.
Mismatched Settings Cause Confusing Import Errors
Let me walk you through a real problem I debugged last month. A developer on my team was working on a Vite project, but someone had left moduleResolution: node in the tsconfig. The code compiled fine locally—the bundler ran without issues. But TypeScript was unhappy about imports from a UI library that had conditional exports.
The error looked like:
Cannot find module '@ui-library' or its corresponding type declarations.But the bundler was resolving it fine at runtime. The problem? TypeScript was using Node's resolution algorithm, which didn't understand the package's exports conditions. The bundler did understand them. Mismatch.
Once we changed to moduleResolution: bundler, the error vanished immediately. TypeScript now validated against the same rules as the bundler.
Here's another case: path aliases. If your tsconfig.json has:
{
"compilerOptions": {
"moduleResolution": "node",
"paths": {
"@/*": ["src/*"]
}
}
}
TypeScript might or might not resolve @/components/Button correctly, depending on the context. But Vite will always resolve it correctly, because Vite explicitly applies path mappings. The mismatch means you can't trust TypeScript's diagnostics.
Worse, you might ship code that TypeScript approves but fails at runtime. Or vice versa—TypeScript rejects code that would actually work.
Common Mistakes
I've seen these patterns repeatedly:
Mistake 1: Using moduleResolution: node in a Vite project. This is the most common one. Vite is not Node.js, and its resolution rules differ. If you're using Vite, set moduleResolution: bundler.
Mistake 2: Not realizing that moduleResolution: bundler is relatively new. It was added in TypeScript 4.7 (mid-2022). If you're maintaining a project that targets older TypeScript versions, you can't use it. But if you're on 4.8 or later, there's no reason not to.
Mistake 3: Mixing bundler with nodeNext. In a monorepo, you might have both app and library packages. Make sure each has the right setting. Apps use bundler, libraries use nodeNext.
Mistake 4: Not checking your bundler's docs for additional resolution rules. Vite and Next.js both have special resolution behavior beyond standard bundler behavior. If you're using advanced features (like Vite's glob imports), you might need additional setup in tsconfig.json.
Key Takeaways
moduleResolution: bundlertells TypeScript to resolve imports the way your bundler (Vite, Webpack, Next.js) does, not the way Node.js does.- It properly supports path aliases, package exports, and conditional exports—all things bundlers handle natively.
- Use
bundlerfor applications with a build step. UsenodeNextfor libraries targeting Node.js directly. - Mismatched resolution settings cause false positives and false negatives—code TypeScript approves but bundlers reject, or vice versa.
- If you're using Vite, Next.js, or any modern bundler,
moduleResolution: bundlershould be in your tsconfig.json.






Comments
Loading comments…