tsc’s production compilation settings are often left at defaults, leading to bloated bundles and slower builds than necessary.

Let’s see what a streamlined tsconfig.json for production looks like in action. Imagine we have a small project:

.
├── src/
│   └── index.ts
└── tsconfig.json

And src/index.ts contains:

function greet(name: string): string {
  return `Hello, ${name}!`;
}

const message = greet("World");
console.log(message);

Here’s a tsconfig.json that’s heavily tuned for production:

{
  "compilerOptions": {
    "target": "es2020",          // Target modern JS for smaller code and better perf
    "module": "esnext",          // Use ES Modules for tree-shaking
    "lib": ["dom", "es2020"],    // Only include necessary environment APIs
    "strict": true,              // Enable all strict type-checking options
    "esModuleInterop": true,     // Allows default imports from CommonJS modules
    "skipLibCheck": true,        // Don't check types in declaration files
    "forceConsistentCasingInFileNames": true, // Prevent casing issues
    "resolveJsonModule": true,   // Allow importing JSON files
    "outDir": "./dist",          // Output directory for compiled JS
    "rootDir": "./src",          // Root directory of source files
    "declaration": true,         // Generate .d.ts files for type definitions
    "sourceMap": true,           // Generate source maps for debugging
    "removeComments": true,      // Remove comments from compiled JS
    "isolatedModules": true,     // Ensure each file can be a module (required for some bundlers)
    "noEmit": false,             // Emit output files
    "incremental": true,         // Enable incremental compilation
    "tsBuildInfoFile": "./.tsbuildinfo" // Location for incremental build cache
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "**/*.spec.ts"]
}

When we run tsc with this configuration:

npx tsc

The output in ./dist will contain index.js and index.d.ts. The index.js will look something like this (depending on exact tsc version and defaults):

function greet(n) {
    return `Hello, ${n}!`;
}
const message = greet("World");
console.log(message);

Notice how comments are gone, and variable names might be slightly shortened by the compiler if certain aggressive optimizations were enabled (though tsc itself doesn’t do heavy minification, bundlers do). The key here is that the structure of the output is optimized for modern JavaScript environments and tooling.

The problem this solves is that by default, tsc is often configured to be broadly compatible, even with older JavaScript runtimes and module systems. This results in more verbose JavaScript output than necessary, which directly impacts:

  1. Bundle Size: Larger JavaScript files mean longer download times for users.
  2. Parse/Execution Time: More code takes longer for the browser or Node.js to parse and start executing.
  3. Tooling Efficiency: Modern bundlers (like Webpack, Rollup, esbuild) can perform better tree-shaking and optimizations when given modern module formats.

Let’s break down the key compilerOptions that achieve this:

  • "target": "es2020": This tells tsc to compile down to ECMAScript 2020. This is a sweet spot. It’s modern enough to allow for concise syntax (like arrow functions, const/let, template literals) that directly maps to efficient JavaScript, but not so bleeding-edge that it requires transpilation to older targets. It means tsc won’t insert polyfills for features that are standard in es2020 environments.
  • "module": "esnext": This is crucial for modern build pipelines. esnext (which is essentially the latest proposed ES Module spec) allows for static analysis by bundlers. This enables powerful tree-shaking, where unused code is automatically removed from the final bundle. If you were to use "module": "commonjs", your output would include require and module.exports statements, which are less amenable to tree-shaking and generally produce larger output.
  • "lib": ["dom", "es2020"]: This tells the TypeScript compiler which built-in APIs are available in your target environment. By specifying "es2020", you’re telling tsc that features from ES2020 (like Promise.allSettled, String.prototype.matchAll) are natively available. By not including older lib entries (like "es5" or "es6"), you avoid tsc generating code that polyfills these features, keeping the output leaner. "dom" is included if you’re building for the browser.
  • "esModuleInterop": true: This is a pragmatic setting. Many older npm packages were written using CommonJS module.exports = ... syntax. Without esModuleInterop, importing these into TypeScript can be awkward (import * as packageName from 'package-name'). With it, you can use the more natural import packageName from 'package-name', and tsc handles the interop, making your code cleaner. While it adds a tiny bit of overhead in the generated JS, it’s usually a net positive for developer experience and compatibility.
  • "skipLibCheck": true: For large projects or projects with many dependencies, checking the type definitions (.d.ts files) in node_modules can significantly slow down compilation. Since these are usually well-typed by their authors, skipping this check for library files can dramatically speed up builds without sacrificing type safety in your own code.
  • "removeComments": true: A straightforward optimization. Comments add to file size and have no runtime effect. Removing them is a simple win for production builds.
  • "isolatedModules": true: This compiler option enforces that each file can be independently transpiled into JavaScript. This is a requirement for many modern bundlers and build tools (like Webpack with ts-loader or Babel with @babel/preset-typescript) to work correctly, especially when they perform their own module transformations and optimizations. It prevents certain TypeScript features that rely on cross-file analysis during compilation from being used, ensuring a smooth integration with downstream tooling.
  • "incremental": true and "tsBuildInfoFile": "./.tsbuildinfo": These two work together. "incremental": true enables TypeScript to remember information from the previous compilation. When you run tsc again, it only recompiles files that have changed and their dependents, rather than rebuilding the entire project from scratch. "tsBuildInfoFile" specifies where this cached information is stored. This dramatically speeds up subsequent builds, especially in large projects.

The most impactful settings for production optimization are usually "target", "module", and ensuring your "lib" array is minimal. The others are more about build speed, developer experience, or compatibility with modern tooling.

When you compile with these settings, your output JavaScript is closer to what a modern browser or Node.js runtime expects, and it’s in a format that downstream tools like bundlers can optimize most effectively.

The next hurdle you’ll likely encounter is realizing that tsc itself doesn’t perform code minification or advanced dead code elimination.

Want structured learning?

Take the full Typescript course →