Back to stories
<Frontend/>

TypeScript for Frontend (Part 2): Bundle Size and Tree-Shaking

Share by

TypeScript for Frontend (Part 2): Bundle Size and Tree-Shaking

TypeScript types and interfaces are removed when you compile to JavaScript. So TypeScript itself does not increase bundle size or runtime cost. What does affect size is how you write code and what you import. This post explains how to keep TypeScript-friendly code bundle-friendly and why tree-shaking matters.


Types and interfaces are erased

Everything in TypeScript that exists only in the type system—interface, type, generic parameters, etc.—is stripped at compile time. The emitted JavaScript has no traces of them. So adding more types does not make your bundle larger or slower.

The only things that can end up in the bundle are:

  • Values: variables, functions, classes, objects, enums (if not const enum).
  • Imports of those values from other modules.

So "TypeScript vs JavaScript bundle size" is really "how you structure and import code" plus "how well your bundler tree-shakes."


Const enums and inlining

A normal enum in TypeScript becomes an object in JavaScript and stays in the bundle. A const enum is inlined by the compiler—each use is replaced by the literal value—so it can disappear from the output.

const enum Status {
  Idle = 0,
  Loading = 1,
  Done = 2,
}
const s = Status.Loading // emits: const s = 1

If you need a small runtime set of named constants and want no bundle cost, const enum helps. Not all build pipelines support const enum (e.g. Babel); in that case a plain object or string union type might be simpler.


Barrel files and tree-shaking

A barrel file re-exports many things from one entry. If you do import { fnA } from './utils', a good bundler will tree-shake and include only the code for fnA. But barrels can also pull in more than you expect if re-exports have side effects or the bundler is conservative.

Prefer direct imports when you want to be sure only one module is included: import { fnA } from './utils/a'. For libraries, avoid barrels that run code at import time.


Import only what you need

// Heavy: might pull in a lot
import _ from 'lodash'

// Lighter: one function
import debounce from 'lodash/debounce'

Same idea for your own code: import specific functions or components instead of a big aggregate. That gives the bundler clear boundaries for tree-shaking.


Type-only imports

If you import a type from a module that also has runtime code, use type-only imports so the runtime part can be dropped when unused:

import type { SomeType } from './types'
import { helper } from './utils'

That way ./types is treated as type-only and doesn't force runtime code into the bundle when you only need the type.


Summary

  • TypeScript types and interfaces are erased at build time; they do not increase bundle size or runtime cost.
  • Const enums can be inlined and leave no runtime object; use where your toolchain allows.
  • Barrel files can hurt tree-shaking; prefer direct imports when you care about size.
  • Import only what you need (e.g. lodash per function) and use type-only imports for types so the bundler can keep bundles small.