🚀AdvancedAdvanced Integration

TypeScript Performance Optimization

Identify and fix slow TypeScript compilation: simplify complex types, avoid distributive inference traps, use `interface` over `type` for merging, and structure projects for incremental builds.

Overview

TypeScript compilation speed matters at scale. A project with 1,000 files can check in under a second - or take 30+ seconds - depending on how types are written. The TypeScript team provides tools to diagnose what is slow and why. Performance problems come from two places: 1. **Type complexity**: deeply recursive conditional types, large union distributions, excessive use of `infer` in hot paths. The checker must evaluate these types at every usage site. 2. **Project structure**: all files in one tsconfig (no incremental caching), circular module imports, unnecessarily large `include` globs pulling in node_modules. The diagnostic tools are: - `tsc --diagnostics` and `tsc --extendedDiagnostics` - print timing breakdowns - `--generateTrace` - emit a Chrome DevTools trace you can analyze visually - `--noResolve` and `--noLib` - isolate what is slow (project vs type resolution) Knowing what to look for lets you fix a 30-second check in an afternoon.

Key Points

  • •`tsc --extendedDiagnostics` shows time per phase: program creation, type checking, emit
  • •`tsc --generateTrace ./trace` produces a Chrome-loadable trace file - open in `chrome://tracing`
  • •Interfaces are faster than type aliases for object shapes because they are cached by identity
  • •Avoid deeply recursive types where N > 20-30 levels - TypeScript has recursion limits
  • •Large union types (>100 members) that distribute through conditional types are slow
  • •Use `interface` over `type` for extended shapes; use `type` only for unions, primitives, mapped types
  • •Isolating types into `isolatedModules: true` enables faster parallel checking in bundlers

Examples

Diagnosing slow compilation with built-in tools

// Run extended diagnostics
// tsc --extendedDiagnostics

// Sample output:
// Files:                         1,842
// Lines of Library:             37,521
// Lines of Definitions:        118,842
// Lines of TypeScript:         124,205
// Parse time:                    0.97s
// Bind time:                     0.40s
// Check time:                    7.22s   <-- the expensive phase
// transformTime time:            0.14s
// Emit time:                     0.22s
// Total time:                    8.97s

// If "Check time" is very high, you have complex types.
// If "Parse time" is high, you are parsing too many files (check your include glob).

// Generate a trace file for visual analysis
// tsc --generateTrace ./build-trace

// Open chrome://tracing in Chrome, load ./build-trace/trace.json
// Filter by "checkExpression" and "checkType" to see hot spots
// Long bars labeled with type names tell you which types are expensive

// Key metrics to watch in trace:
// - "checkType" events > 100ms on one type: that type is too complex
// - Many small "checkExpression" events: often caused by large unions in hot function

Interface vs type alias performance

// SLOWER: type alias for object shapes
// Type aliases are re-evaluated at every usage site
type SlowUserA = {
  id: string;
  name: string;
  email: string;
  role: 'admin' | 'user' | 'guest';
};

type SlowUserB = {
  createdAt: Date;
  updatedAt: Date;
};

// This intersection is re-computed each time it's used
type SlowUser = SlowUserA & SlowUserB;

// FASTER: interface for object shapes
// Interfaces are cached by identity in the type checker
interface FastUserA {
  id: string;
  name: string;
  email: string;
  role: 'admin' | 'user' | 'guest';
}

interface FastUserB {
  createdAt: Date;
  updatedAt: Date;
}

// Interface extends is faster than type intersection
interface FastUser extends FastUserA, FastUserB {}

// Why: TypeScript interns interfaces - it recognizes FastUser by its
// declared identity, not by structural shape-matching each time.
// Type alias intersections require re-computation at every use site.

// Rule: use 'interface' for object shapes that will be extended or frequently referenced
// Use 'type' for: unions, primitives, mapped types, conditional types

Avoiding expensive type patterns

// SLOW: deeply recursive conditional type
// TypeScript must evaluate this up to 50 levels deep for each usage
type DeepReadonly<T> = T extends (infer U)[]
  ? ReadonlyArray<DeepReadonly<U>>
  : T extends object
  ? { readonly [K in keyof T]: DeepReadonly<T[K]> }
  : T;

// For deeply nested objects, this can be very slow.
// Consider limiting depth or using a library that handles this.

// SLOW: large string union distribution
type Events = 'click' | 'focus' | 'blur' | 'input' | 'change' | 'submit'
            | 'keydown' | 'keyup' | 'mouseenter' | 'mouseleave'
            | 'touchstart' | 'touchend' | 'scroll' | 'resize'
            | 'load' | 'unload' | 'error' | 'abort';

// Distributing 18 members through a conditional type = 18 evaluations per usage
type HTMLEvents = Events extends infer E ? E extends string ? `on${Capitalize<string & E>}` : never : never;

// FAST: use mapped type instead of distributive conditional type
type HTMLEventsMap = { [E in Events]: `on${Capitalize<E>}` };
type HTMLEventsFast = HTMLEventsMap[Events];  // Look up all at once

// FAST: use const assertion for fixed data instead of complex inference
const ROLES = ['admin', 'user', 'guest'] as const;
type Role = typeof ROLES[number]; // Derived once, used many times

// FAST: extract repeated type computations to named types
// Instead of writing Pick<User, 'id' | 'name'> everywhere:
type UserSummary = Pick<FastUser, 'id' | 'name'>;
// TypeScript caches the named type; inline writes are re-evaluated each time

Common Mistakes

  • Using `type` for every type definition out of habit: `interface` for objects that extend other objects is measurably faster for large projects
  • Writing extremely recursive utility types without depth limits: TypeScript will not compile types that recurse more than ~100 levels, and performance degrades well before that limit
  • Large `include` globs that pull in `node_modules` or test fixtures: always use explicit `include` arrays or exclude `node_modules` and generated directories
  • Not using project references for large codebases: a single tsconfig forces full re-checking on every change, while project references enable incremental caching per package

Note

The single most impactful thing you can do for TypeScript performance in a large codebase is split it into project references (see TS039). The second most impactful thing is to run `tsc --generateTrace` and spend 30 minutes in `chrome://tracing` identifying the two or three types that dominate check time. Usually a handful of complex types account for the majority of compile time.

Exercises

Write TypeScript code to solve each exercise. Click Run to compile and execute it right here, then mark as done.

Loading editor…

Completed: 0 / 2 exercises

Knowledge Check

Question 1 of 4

Which TypeScript CLI flag gives you a per-phase timing breakdown to identify whether slow compilation is due to parsing, type checking, or emit?