Keeping the Compiler Fast

Advanced · 18 min read · ▶ live playground · ✦ checkpoint

Keeping the compiler fast is part of TypeScript design. The checker is not a background tax you helplessly pay; your type shapes, file graph, and build setup decide how much work it has to do.

This lesson is tool-shaped. The examples are reference code, tsconfig files, and terminal commands you would adapt in a real project.

What makes tsc slow

The compiler has ordinary scale costs: more files, more declarations, more dependencies. It also has shape costs. Some type patterns ask the checker to compare, distribute, or instantiate many possibilities.

Three usual suspects:

  • Huge unions: every use may require checking many members.
  • Deep conditional and mapped types: especially when they distribute over unions or recurse through nested structures.
  • Barrel files: convenient index.ts files that re-export broad surfaces can make the compiler and editor walk more of the project than one file really needs.

Reference code:

type Locale = "en" | "es" | "fr";
type Page = "home" | "pricing" | "learn";
type Slot = "title" | "subtitle" | "cta";
 
type MessageKey = `${Locale}.${Page}.${Slot}`;
 
type ApiResponse<T> =
  T extends { ok: true; data: infer Data }
    ? Data
    : T extends { ok: false; error: infer ErrorInfo }
      ? ErrorInfo
      : never;

This example is fine. The warning is about the scaled-up version: dozens of union members multiplied through template literal types, then fed through conditional helpers, then exported from a barrel that every package imports. The type system is doing exactly what you asked; you asked it to build a large search space.

The same applies to barrels. A package-level public barrel can be a good API. A deep internal barrel that re-exports every helper from every folder often becomes a graph amplifier. For internal code, prefer importing from the module that owns the thing you need. Save broad barrels for intentional package boundaries.

Incremental builds and project references

incremental tells TypeScript to save project-graph information in .tsbuildinfo files, so later builds can reuse what did not change. It does not make the first build free. It makes repeated builds less wasteful.

Reference config:

{
  "compilerOptions": {
    "composite": true,
    "declaration": true,
    "declarationMap": true,
    "incremental": true,
    "rootDir": "src",
    "outDir": "dist",
    "tsBuildInfoFile": "dist/core.tsbuildinfo",
    "strict": true
  },
  "include": ["src/**/*.ts"]
}

composite is the project-reference-ready setting. It adds constraints that let TypeScript know where outputs are and whether a referenced project is up to date. declaration and declarationMap make referenced projects usable through their .d.ts outputs while preserving editor navigation.

A solution config connects the pieces:

{
  "files": [],
  "references": [
    { "path": "packages/core" },
    { "path": "packages/web" },
    { "path": "packages/worker" }
  ]
}

Then build mode becomes the entry point:

npx tsc -b
npx tsc -b --verbose
npx tsc -b --dry
npx tsc -b --clean

tsc -b finds referenced projects, checks whether they are up to date, and builds out-of-date projects in dependency order. --verbose explains what it is doing. --dry shows what would be built. --clean deletes outputs for the referenced build.

Use this when your project has real boundaries: packages, apps, workers, libraries, or generated clients. Do not split a small app into six projects just to look mature. Project references help when boundaries are meaningful enough that TypeScript can avoid rechecking everything every time.

Measuring before guessing

When typecheck time hurts, measure the compiler before rewriting types.

Start broad:

npx tsc --noEmit --diagnostics
npx tsc --noEmit --extendedDiagnostics
npx tsc --noEmit --generateTrace ./.trace/tsc --incremental false

--diagnostics prints timing and size information for debugging compiler work. --extendedDiagnostics is the more detailed, easier-to-read sibling many teams reach for first. --generateTrace writes trace data and type information to a directory so you can inspect where time went.

The output shape looks like this:

Files:              284
Types:            52110
Instantiations:   88402
Memory used:    188432K
Parse time:        0.61s
Bind time:         0.22s
Check time:        4.87s
Total time:        5.98s

Treat those numbers as a before/after baseline. If Check time dominates and instantiations are high, look for type-level expansion: distributed conditionals, generated unions, deep mapped helpers, and broad public aliases. If parse or file counts dominate, look at file graph shape: include, accidental test/build output inclusion, huge declaration packages, and barrel imports.

For traces, keep the workflow contained: delete the old trace directory first, write the new trace under .trace/tsc, and keep --incremental false when you want to see the actual compile work instead of a mostly skipped incremental run.

Trace directories can be large and noisy. Do not commit them. Use them to identify a hot file or expression, reduce the case, then simplify the real type or file graph.

The TypeScript 7 effect

The course's established TypeScript 7 framing is: Go-native compiler and language service work, roughly 10x more headroom, same TypeScript language. That is excellent news for large projects.

It is not permission to spend the entire speedup on type cleverness.

If a generated type maze is hard to explain today, it will still be hard to explain with a faster compiler. If a barrel makes ownership blurry today, faster graph walking will not make ownership clear. If CI is barely acceptable today, a 10x improvement should buy faster feedback and room for growth, not a new baseline of "barely acceptable."

Use the headroom to keep the loop humane: quick editor startup, quick hover, quick test-and-typecheck, quick CI. When a type pattern makes those loops slower, require the same standard you would require of runtime code: prove the value, measure the cost, and simplify when the trade is bad.

Section 17 is now complete: lint policy, formatting hygiene, debugging, and compiler performance. Next section moves from project tooling into publishing and sharing typed code with other consumers.

Checkpoint

Answer all three to mark this lesson complete

+50 XP on completion