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
171: Module and Target Interactions
I've seen a lot of developers treat the target and module settings in tsconfig.json as if they're the same thing. On the surface, they both look like they're just asking "Which version of JavaScript do you want?" But they are actually controlling two completely different stages of the pipeline. target is about the syntax of your code—things like arrow functions, async/await, and classes. module is about the transport—how files talk to each other via import and export.
The "Latest and Greatest" Trap
When I first started with TypeScript, my instinct was to just set everything to ESNext. It felt right; why wouldn't I want the most modern JS possible? Let's say you're building a TelemetryClient that tracks app performance. You write some clean, modern code:
import { Timer } from './utils';
export class TelemetryClient {
async sendEvent(name: string) {
const duration = Timer.now();
await fetch('/log', { method: 'POST', body: JSON.stringify({ name, duration }) });
}
}
If you set both target: "ESNext" and module: "ESNext", TypeScript basically steps out of the way. It leaves your async/await alone and leaves your import/export statements exactly as they are. This works great—until you try to run that code in a Node.js environment that isn't configured for ESM, or an older browser. You'll get a crash the moment the engine hits an import statement it doesn't recognize, or an async keyword it can't parse.
The "Safe Bet" Performance Hit
The opposite reaction is the "safe bet": setting target: "ES5" and module: "CommonJS". This is the old-school way. TypeScript will dutifully transform your async/await into a giant mess of state-machine promises and change your import statements into require() calls. While this code runs almost anywhere, you've just killed your ability to use "tree-shaking."
Since CommonJS modules are evaluated at runtime, most modern bundlers (like Vite or Webpack) can't statically analyze which parts of your TelemetryClient are actually being used. If your library grows to 100kb, but the user only uses one function, they're still shipping all 100kb to the browser. I've spent way too many hours debugging bloated bundle sizes only to realize the module setting was forcing everything into a monolithic CommonJS blob.
Finding the Sweet Spot
The professional way to handle this is to decouple your syntax target from your module system based on who is actually executing the code. In a modern web project, I almost always recommend module: "ESNext" regardless of the target. Why? Because you aren't usually shipping the raw TS output to the browser; you're shipping it to a bundler. Bundlers love ESM; it's what allows them to prune dead code.
However, your target should be a calculated decision based on your user base. If you need to support browsers from four years ago, target: "ES2017" or "ES2018" is often the sweet spot. It gives you async/await (which is natively supported and much faster than the transpiled ES5 version) but doesn't rely on the absolute bleeding edge of the spec.
Here is how I usually configure a modern library meant for broad consumption:
{
"compilerOptions": {
"target": "ES2020", // Use modern syntax, but not "Next" to avoid unstable features
"module": "ESNext", // Keep imports/exports intact for the bundler to optimize
"moduleResolution": "node",
"esModuleInterop": true
}
}
By doing this, you're telling TypeScript: "Convert my fancy new class fields or nullish coalescing into something 2020-era JS understands, but leave the module structure alone so my build tool can optimize the final package." It's a trade-off that prioritizes runtime performance and bundle size over "maximum compatibility," which is usually the right call for any project not targeting Internet Explorer 11.
📋 Practical Task
Fixing the Bundle Bloat in the "Analytics-Core" Library
You've inherited a project called analytics-core. The current tsconfig.json is causing the production bundle to be massive because the bundler cannot perform tree-shaking, and the code is being transpiled down to an unnecessarily old version of JavaScript, causing slow execution of async functions.
Current tsconfig.json:
{ "compilerOptions": { "target": "ES5", "module": "CommonJS", "strict": true, "outDir": "./dist" } }The Goal:
- Change the
modulesetting so that modern bundlers can perform tree-shaking (dead code elimination). - Update the
targetto a modern version (ES2020 or newer) to ensureasync/awaitis handled natively by the engine rather than being transpiled into complex ES5 helper functions.
Modify the tsconfig.json to reflect these architectural improvements.
There are no comments for now.