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
168: The tsconfig.json Reference In Depth
I've seen this happen a dozen times with developers moving from older Node projects to modern ESM (ES Modules). You've got a project that looks perfect in VS Code—no red squiggles anywhere—but the second you run tsc in your terminal, it explodes with a wall of "Cannot find module" errors.
// src/index.ts
import { logger } from './utils/logger.js';
console.log(logger.log('System started'));
The error looks something like this: Cannot find module './utils/logger.js'. Did you mean './utils/logger'?
Wait, why is TypeScript complaining about the .js extension? You're writing TypeScript! You've probably tried removing the extension, but then Node.js crashes at runtime because ESM requires explicit extensions. You're stuck in a loop where the IDE is happy, the compiler is angry, and the runtime is confused.
The mismatch between Module and ModuleResolution
The culprit here is almost always a misalignment in your tsconfig.json. In older TypeScript projects, we used "module": "commonjs" and "moduleResolution": "node". The node resolution strategy (now often called node10) is "lazy"—it looks for .ts, then .tsx, then /index.ts, and it doesn't care if you omit the extension.
But modern Node.js uses a much stricter resolution algorithm. If you're using "type": "module" in your package.json, your tsconfig.json needs to reflect that. If your config looks like this:
{ "compilerOptions": { "target": "ESNext", "module": "ESNext", "moduleResolution": "node" } }...you're telling TypeScript to output ESM code, but to look up files using the old CommonJS rules. That's why
tschates your.jsextensions.Aligning the compiler with the runtime
To fix this, you need to move to
NodeNext(orNode16). This is a "combo" setting that forces TypeScript to follow the actual rules Node.js uses for ESM.{ "compilerOptions": { "target": "ESNext", "module": "NodeNext", "moduleResolution": "NodeNext" } }Once you make this change, TypeScript finally understands why you're importing a
.jsfile while writing a.tsfile. It realizes that in the final emitted code, that extension must exist for Node to find the file. The errors vanish, and your build actually works.Taming the Strictness Bundle
You've likely already seen the
"strict": trueflag. I usually tell people to just leave it on, but as you get deeper into thetsconfigreference, you should know thatstrictis actually a "meta-flag." It's a shortcut for a whole group of settings.If you find
strict: trueis too aggressive for a legacy codebase you're migrating, you can turn it off and selectively enable the ones that actually matter. The most critical ones are:
strictNullChecks: This is the one that prevents 90% of "cannot read property of undefined" crashes. Keep this on at all costs.noImplicitAny: Stops you from accidentally opting out of type checking by forgetting a type annotation.strictFunctionTypes: Ensures your function arguments are checked more rigorously.
I've seen engineers disable strict entirely just to get a project to compile, but that's like taking the brakes off your car because you're tired of stopping at red lights. It's faster for a minute, but the crash is inevitable.
Solving the Import Spaghetti with Paths
Eventually, your imports start looking like this: import { User } from '../../../core/models/user'. It's ugly and fragile. You can fix this using baseUrl and paths, but there's a catch that trips everyone up.
{ "compilerOptions": { "baseUrl": ".", "paths": { "@core/*": ["src/core/*"] } } }Here is the "gotcha":
pathsis a compiler-only feature. TypeScript will resolve these aliases during type checking, but it will not rewrite them to relative paths when it emits the JavaScript. If you run the resulting JS in Node, it will throw a "Module not found" error because Node has no idea what@core/means.If you use aliases, you'll need a runtime helper like
tsconfig-pathsor use the nativeimportsfield inpackage.json. Don't assume that because thetsconfigis happy, the code will actually run.The "Quiet" Flags that Save Your Build
When you start importing large third-party libraries, you might see TypeScript complaining about types inside those libraries—files you didn't even write. It's incredibly frustrating to have a build fail because a random npm package has a type mismatch in its own internal files.
This is where
"skipLibCheck": truebecomes your best friend. It tells TypeScript: "Don't type-check the.d.tsfiles innode_modules. I trust the library authors (mostly), and I just want to check my own code." In any professional project, this is almost always set totrueto keep build times down and sanity levels up.
📋 Practical Task
Exercise: Repairing the "Broken Alias" Configuration
You have been handed a project with a broken tsconfig.json. The developer tried to implement path aliases and modern ESM, but the configuration is conflicting, causing the compiler to fail and the imports to be unresolved.
The Scenario:
- The project uses
"type": "module"inpackage.json. - The developer is using
import { api } from '@services/api.js'. - The current
tsconfig.jsonis using"moduleResolution": "node"and has a misplacedbaseUrl.
Your Task:
Edit the following tsconfig.json to:
- Update the
moduleandmoduleResolutionto support modern Node.js ESM. - Fix the
pathsconfiguration so that@services/*correctly maps to thesrc/services/directory. - Ensure that third-party type errors in
node_modulesare ignored to speed up compilation.
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "node",
"baseUrl": "src",
"paths": {
"@services/*": ["services/*"]
},
"strict": true,
"skipLibCheck": false
}
}
There are no comments for now.