TypeScript
Completed
-
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
51: Module Resolution Strategies
A few years ago, I was helping a colleague migrate a legacy internal tool from CommonJS to ES Modules. He had just changed his package.json to "type": "module" and updated his tsconfig.json to "module": "NodeNext". He came to me completely baffled because his IDE was suddenly bleeding red. He had a file called auth-service.ts, and he was importing it as import { login } from './auth-service';. He told me, "The file is right there in the same folder. Why is TypeScript telling me it can't find the module?"
This is the exact moment where most developers hit a wall with module resolution. It feels like a regression because, for years, we've been trained to omit file extensions in our imports. But as TypeScript aligns more closely with how Node.js actually handles ES Modules, the rules of the game have changed.
The Shift from Node10 to NodeNext
In your tsconfig.json, the moduleResolution setting tells TypeScript exactly which algorithm to use to find the file you're trying to import. For a long time, node10 (formerly just called node) was the gold standard. It's a permissive strategy: it looks for a .ts file, then a .tsx file, then checks if there's an index.ts inside a folder of the same name. It's convenient, but it's essentially a "guess" that doesn't always match how the code actually executes in a modern runtime.
When you switch to node16 or nodenext, you're telling TypeScript to stop guessing and start following the strict rules of Node.js's ESM implementation. These strategies are tied to the module setting. If you set "module": "NodeNext", TypeScript automatically sets the resolution strategy to match. The biggest change here is that the runtime now requires explicit extensions. If you're using ESM, Node.js wants to know exactly which file it's loading without searching the file system for candidates.
The Extension Paradox
This leads to the part that feels totally wrong when you first see it: you have to import the .js extension, even though you are writing a .ts file. I know it feels like a lie. You're looking at user-model.ts on your disk, but you write import { User } from './user-model.js';.
// This works in node10, but fails in nodenext
import { validate } from './validator';
// This is required in nodenext
import { validate } from './validator.js';
Why? Because TypeScript doesn't rewrite your import paths during compilation. It assumes that since the code will eventually be executed as JavaScript, the import path should reflect the final output. If you're targeting a modern Node environment, the .js extension is mandatory for ESM. It's a bit mind-bending at first, but it's the only way to ensure your code is actually compatible with the runtime without relying on heavy bundling tools like Webpack or Vite to "fix" your paths for you.
Handling Package Exports
Beyond just file extensions, NodeNext resolution respects the exports field in a library's package.json. In the old node10 days, if a package had a main entry point, you could often reach deep into the package's folder structure—like import { internal } from 'some-lib/dist/internal/utils'. Modern resolution strategies stop this.
If a library defines an exports map, TypeScript will only let you import the specific entry points the author has explicitly exposed. If you try to import something not listed in that map, you'll get a module resolution error, even if the file physically exists in node_modules. This is actually a great feature for library authors to hide internal implementation details, but it can be a headache for you if you're relying on a poorly configured third-party package.
📋 Practical Task
Fixing "Module Not Found" in a NodeNext Migration
You have been handed a small project that is being migrated to NodeNext. The developer has already updated the tsconfig.json and package.json, but the code is now full of resolution errors. Your task is to fix the imports so the project compiles under the NodeNext strategy.
Current Project Structure:
package.json(contains"type": "module")tsconfig.json(contains"module": "NodeNext"and"moduleResolution": "NodeNext")src/index.tssrc/logger.tssrc/utils/formatter.ts
The broken src/index.ts:
import { logInfo } from './logger';
import { formatCurrency } from './utils/formatter';
logInfo(formatCurrency(100));
The broken src/logger.ts:
import { formatCurrency } from './utils/formatter';
export function logInfo(msg: string) {
console.log(`[LOG]: ${msg}`);
}
Your Task: Rewrite the import statements in src/index.ts and src/logger.ts to satisfy the NodeNext resolution requirements. Ensure that the imports reflect the final compiled output paths.
There are no comments for now.