ToolSite

TypeScript to Production: Minify Your Bundle

TypeScript to production: compile, bundle, tree-shake, mangle. Enum bloat, side effects, target tradeoffs. Real numbers. Use our free JS/TS minifier tool.

By ToolSite5 min readguides

TypeScript Does Not Ship

Browsers and Node.js do not run TypeScript. They run JavaScript. Before minification can happen, your .ts files must be compiled to .js. The minifier receives JavaScript output, not TypeScript source, and shrinks it.

The full production pipeline:

TypeScript source -> Compile -> JavaScript -> Bundle -> Minify -> Production artifact

Each step removes or transforms something. Understanding the chain helps you debug when the production output behaves differently from the source.

Step 1: Compile (tsc, esbuild, swc)

The TypeScript compiler strips type annotations, interface declarations, and type-only imports. It may also downlevel modern syntax to older JavaScript targets:

// Source: 132 characters
const greet = (name: string): string => `Hello, ${name}`;
interface User { id: number; name: string; email: string; }
type Status = "active" | "inactive";

// Compiled (target: ES2020): 50 characters
const greet = (name) => `Hello, ${name}`;
// interface User is gone entirely
// type Status is gone entirely

Type annotations consume zero bytes in the compiled output. The compiler's job is to strip them and emit valid JavaScript. Interfaces, type aliases, generics, and type guards all disappear. They exist only at compile time.

Compilation Targets and Output Size

The target you choose matters for output size:

| Target | class syntax | async/await | Output size (relative) | |---|---|---|---| | ES5 | Transpiled to prototype | Transpiled to generators | Largest | | ES2015 | Native class | Transpiled to generators | Medium | | ES2017 | Native class | Native async/await | Smaller | | ES2020+ | All native syntax | All native syntax | Smallest |

When you target ES5, every class compiles to a verbose prototype-based helper function. A single class Foo { ... } becomes roughly 800 bytes of ES5 code plus a shared __extends helper. Target ES2015 or later for modern runtimes. The compiled output is nearly identical to the source and minifies cleanly.

Step 2: Bundle (Webpack, esbuild, Rollup)

The bundler creates a dependency graph, resolves imports, and produces one or more output files. During bundling, tree-shaking removes exports that are never imported:

// utils.ts: 2 functions exported
export function used() { return 42; }
export function unused() { return expensiveOperation(); }

// main.ts: only imports 'used'
import { used } from './utils';
console.log(used());

The bundler sees that unused is never imported and eliminates it from the final output. This is the single largest source of size reduction in modern TypeScript projects. A library with 50 exported functions where your app only imports 3 will see 47 functions stripped at the bundle stage.

Step 3: Minify (Terser, esbuild, SWC)

The minifier processes the bundled JavaScript and applies these transformations:

  • Strips all whitespace and comments.
  • Shortens identifiers to single letters (mangling).
  • Removes dead code branches the bundler could not eliminate.
  • Inlines single-use variables where the variable name is longer than the value.
  • Simplifies boolean expressions and constant arithmetic.

A 200 KB bundled output might become 65 KB after minification and roughly 18 KB after gzip. The three passes (compile, bundle, minify) reduce the shipped byte count by 80 to 90 percent from the original TypeScript source.

Common Pitfalls

Side Effects Block Tree-Shaking

The bundler assumes any import that runs top-level code must be kept:

// polyfills.ts
if (!Array.prototype.flat) {
  Array.prototype.flat = function() { /* implementation */ };
}

// main.ts
import './polyfills'; // Nothing imported, but side effects run

Even though main.ts imports no exports from polyfills.ts, the bundler keeps the file because it modifies a global. Mark modules with no side effects in package.json:

{
  "sideEffects": false
}

Or list specific files that have side effects:

{
  "sideEffects": ["./src/polyfills.ts", "*.css"]
}

This tells the bundler it is safe to remove unused exports from all other files.

Enum Overhead

TypeScript enums generate JavaScript that survives minification and adds unexpected weight:

// Source
enum Color { Red, Green, Blue }

Compiles to roughly 200 bytes of JavaScript:

var Color;
(function (Color) {
  Color[Color["Red"] = 0] = "Red";
  Color[Color["Green"] = 1] = "Green";
  Color[Color["Blue"] = 2] = "Blue";
})(Color || (Color = {}));

One enum is negligible. Fifty enums add 10 KB to your bundle. Alternatives:

  • const enum: inlined at compile time with zero runtime output. Only works when the enum is not exported across module boundaries.
  • String literal unions: type Color = "red" | "green" | "blue". Zero runtime cost. Use an object for reverse mapping if needed.
// Zero runtime bytes
type Color = "red" | "green" | "blue";
const COLORS = ["red", "green", "blue"] as const;

Barrel Exports and Re-Exports

Barrel files (index.ts that re-exports from many modules) defeat tree-shaking in some bundlers:

// index.ts
export { Button } from './Button';
export { Modal } from './Modal';
export { Tooltip } from './Tooltip';

If your app only uses Button, older bundlers might still pull in Modal and Tooltip because the barrel file evaluates all re-exports. Modern bundlers (esbuild, Rollup with proper config, Webpack 5 in production mode) handle this correctly. Verify with a bundle analyzer if you ship many barrel files.

Decorator Metadata

TypeScript decorators with emitDecoratorMetadata enabled inject reflect-metadata calls and type information into the compiled output. This metadata survives minification and adds substantial size. Disable emitDecoratorMetadata in production or avoid decorators when bundle size matters.

Try it yourself: open the JS/TS Minifier. Paste a compiled JavaScript function (output from tsc) and click Minify. Compare the character count before and after. Then paste the minified output into a beautifier to confirm the logic is intact. Repeat with a few functions to see how mangling differs based on variable name length.

Related Reading