TypeScript / THE COURSESearch
STAGE 04 / LIBRARIES & SCALE

LESSON 30 OF 36 · 24 MIN + PRACTICE

Compiler & editor performance

Find what slows the compiler or editor before changing your types.

EXAMPLES CHECKED WITH TYPESCRIPT 7.0.2 / NODE 24

Where you are

Your package graph has explicit boundaries. Before changing types for speed, identify the slow command, its inputs, the machine, and the compiler version. Native code improves the baseline but does not make arbitrary type complexity free.

Mental model

Separate program loading, parsing, checking, declaration emit, and editor requests. Runtime benchmarks measure the running JavaScript. A faster JavaScript algorithm does not necessarily make a conditional type cheaper, and a simpler type may change no emitted JavaScript at all.

Runtime and tooling reality

Editor responsiveness includes file watching, filesystem cost, extension activity, project discovery, and language-service requests. A quick CLI check does not prove a slow editor is using the same version or project. Record the selected workspace language server and compare the same file set.

TypeScript model

Large unions, repeated intersections, recursive conditionals, and enormous generated declarations can increase work. Named intermediate types can improve readability and avoid repeated expansion, but do not claim a universal speedup without measurement. Prefer interfaces for ordinary extension when they communicate the model more clearly; measure before rewriting a repository.

TypeScript 7's native architecture supports parallel checking and parallel project builds. More checkers can increase memory. Keep the configuration fixed when comparing results, and record whether the cache is cold or warm. Legacy compiler tracing recipes may not exist unchanged in the native compiler; inspect current help and documentation before recommending a flag.

Working example

Source: examples/valid/30.ts

TypeScript
interface Summary {
  total: number;
  attempts: number;
}
function summarize(jobs: readonly { attempts: number }[]): Summary {
  return { total: jobs.length, attempts: jobs.reduce((sum, job) => sum + job.attempts, 0) };
}
console.log(JSON.stringify(summarize([{ attempts: 2 }])));
export {};

Verified runtime output:

text
{"total":1,"attempts":2}

The simple contract avoids encoding arbitrary recursive JSON traversal at the type level. Its runtime loop is straightforward and independent of compiler tuning.

Measurement lab

Run these from a project root and save the output with compiler and Node versions:

sh
npx tsc --version
npx tsc --help --all
npx tsc --noEmit --extendedDiagnostics

Use the diagnostic timing categories to decide where to investigate. For project references, compare clean and incremental tsc -b separately. Repeat enough times to notice variance; report the median and the machine used. Do not use the release blog's benchmark multiplier as your own measurement.

Type-checker drill

Intentionally invalid: examples/invalid/30.ts

TypeScript
type Endless<T> = Endless<T>[] & Endless<T>;
export {};

Actual TypeScript 7.0.2 diagnostic:

text
examples/invalid/30.ts(1,6): error TS2456: Type alias 'Endless' circularly references itself.
examples/invalid/30.ts(1,19): error TS2315: Type 'Endless' is not generic.
examples/invalid/30.ts(1,34): error TS2315: Type 'Endless' is not generic.

A recursively self-expanding alias cannot produce a finite useful result here. The failure is more basic than slow checking. Replace it with a bounded representation or runtime algorithm instead of increasing recursion complexity until diagnostics become unreadable.

Runtime drill

Compare a simple declared API and a heavily transformed equivalent while inspecting their emitted JavaScript. If the runtime code is identical, a runtime benchmark cannot evaluate the type-checking cost. Conversely, optimizing the type expression will not accelerate network requests.

Professional pattern

Reduce a slow type to a small reproducible case before changing public contracts. Track editor symptoms separately from build timings. Place generated types behind stable interfaces when consumers do not need the entire generated structure. Keep declaration output understandable and avoid forcing downstream users to repeatedly evaluate private transformations.

Exercise

Create two equivalent public contracts: one with a named object model and one with nested mapped/intersection helpers. Measure checking and declaration output on a repeated consumer fixture. Record size, timings, and error quality. Choose the simpler version unless the complex one provides a demonstrated consumer benefit. Document uncertainty if the timing difference is noise.

Checkpoint

  • You separate runtime, compiler, and editor performance.
  • You collect a reproducible baseline before tuning.
  • You understand CPU/memory tradeoffs in native parallelism.
  • You resist complexity without measurable value.

Sources

TypeScript 7 architecture and controls and TypeScript performance guidance.

Check off this lesson when you can meet its checkpoint. Progress stays in this browser.