Skip to Content
Course content

171: Module and Target Interactions

Click on the "Edit" button in the top corner of the screen to edit your slide content.

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 module setting so that modern bundlers can perform tree-shaking (dead code elimination).
  • Update the target to a modern version (ES2020 or newer) to ensure async/await is handled natively by the engine rather than being transpiled into complex ES5 helper functions.

Modify the tsconfig.json to reflect these architectural improvements.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.