Performance & Type Checker Optimization

Advanced TypeScript — Performance & Type Checker Optimization
Advanced TypeScript
Course 4 · Chapter 8 · Performance & Type Checker Optimization

⏱️ Performance & Type Checker Optimization

Course 3's performance chapter was about the running program. This one is about a different program entirely: tsc itself. A project can compile to perfectly fast JavaScript while taking 45 seconds to type-check, or make an editor's language server freeze on every keystroke — and the very techniques this course just spent seven chapters teaching are exactly what can cause it.

Why Type Inference Gets Expensive

Every advanced technique in this course has a cost proportional to how much work the compiler does to resolve it — and that cost compounds:

Union/Intersection Explosion

Chapter 6's template literal types distribute over unions — a template applied to two unions of 20 members each can produce a union of 400 combinations, all of which the checker must track.

Recursion Depth

Chapters 1 and 2's recursive conditional types are re-evaluated at every use site — deep, frequently-used recursive types multiply that cost across the whole project.

Generic Instantiation

Every call to a generic function creates a fresh instantiation for the checker to resolve — Chapter 3's Curried<Args, Return> is cheap once, expensive called thousands of times across a codebase.

Structural Comparison

Checking whether one object type is assignable to another compares every property — the bigger and more deeply nested the shape, the more comparisons per check.

🚧 Common Performance Cliffs

A Real Cliff: Combinatorial Template Literals

Explodes Combinatorially
type Resource = "user" | "order" | "product" | /* ...17 more */; type Action = "create" | "read" | "update" | "delete" | /* ...6 more */; type Permission = `${Resource}:${Action}`; // 20 resources × 10 actions = 200 string literal members — // fine alone, but if this feeds into ANOTHER template literal or // conditional type used everywhere, the cost multiplies from there.
Bounded
// Only generate the specific combinations actually used, // or validate the string shape at runtime (Chapter 4/5's guards // and brands) instead of exhaustively typing every combination. type Permission = Brand<string, "Permission">;

Deeply Nested Conditionals

A conditional type chain ten levels deep, used across hundreds of files, means ten evaluations at every single use — not once.

Large Literal Unions From as const

A mapped type iterating over a 500-entry as const array's keys is doing real, non-trivial work five hundred times.

🏗️ Incremental Builds

The most effective performance fix often has nothing to do with simplifying any individual type — it's making the compiler avoid re-checking work it already did:

Incremental Compilation & Project References

// tsconfig.json — cache what was already checked between runs { "compilerOptions": { "incremental": true, "tsBuildInfoFile": "./.tsbuildinfo" } } // A monorepo split into project references — changing one package // only rechecks that package and whatever depends on it, not everything. { "references": [ { "path": "./packages/core" }, { "path": "./packages/api" }, { "path": "./packages/web" } ] } // Each referenced package needs "composite": true in its own tsconfig.json

.tsbuildinfo

A cache file recording what was already type-checked — tsc --incremental skips unchanged files entirely on the next run.

Project References

Splits a large codebase into independently-checked units — the CI equivalent of this idea is exactly why Course 3's tsc --noEmit pipeline step matters: catching type errors fast, before the slower steps run.

Diagnosing Slowness Instead of Guessing

TypeScript ships its own profiling tools — the same "measure, don't guess" discipline from Course 3's runtime performance chapter applies to the type checker too:

tsc --noEmit --extendedDiagnostics # Prints check time, instantiation counts, and symbol counts — # a huge "Types" or "Instantiations" number points at where to look. tsc --generateTrace ./trace-output # Produces a trace file loadable in chrome://tracing or the # @typescript/analyze-trace tool — shows exactly which specific # type checks took the longest, not just an aggregate number.

💻 Coding Challenges

Challenge 1: Identify the Cliff

Given a type type AllCombos<A extends string, B extends string, C extends string> = \`${A}-${B}-${C}\` applied to three 15-member unions, explain (in a comment, no code needed) roughly how many total string literal members the resulting type has, and why that's a performance concern if reused across many files.

Goal: Practice recognizing combinatorial growth before it becomes a real project's slow build.

→ Solution

Challenge 2: Set Up an Incremental Build

Write a tsconfig.json with "incremental": true and a custom tsBuildInfoFile path, then explain in a comment what changes about a second tsc run compared to the first.

Goal: Practice the configuration that gives the single biggest, easiest performance win for most projects.

→ Solution

Challenge 3: Simplify a Recursive Type

Take Chapter 2's unbounded Repeat<T> (the one that causes "excessively deep and possibly infinite") and rewrite it as the depth-limited version, then explain why the depth-limited version is cheaper for the type checker, not just "more correct."

Goal: Practice connecting a correctness fix from Chapter 2 to its performance benefit, not just its "it no longer errors" benefit.

→ Solution

⚠️ Gotcha: Optimizing Types Nobody Measured

It's tempting to preemptively simplify every clever type this course covered "just in case" it's slow — that's the type-checker equivalent of Course 3's runtime optimization gotcha. Most projects never come close to a real performance cliff; the ones that do usually have one or two identifiable offenders (a giant template-literal union, a recursive type on a hot path) findable with --extendedDiagnostics, not a vague sense that "advanced types are slow." Measure first, simplify the specific thing the trace points at.

🎯 What's Next

Every technique from this course — conditionals, recursion, tuples, guards, brands, type-level programming, augmentation, and now performance awareness — comes together in the final chapter: Advanced Patterns in Practice, combining them into a small, genuinely type-safe framework.