Skip to Content
Course content

89: Common Causes of Slow Type Checking

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I remember a project I worked on a few years ago—a massive internal dashboard for a logistics company. Everything was snappy until one of our lead devs decided to implement a "universal" utility type that could recursively map through any API response to create a strictly typed, read-only version of the data. It looked elegant on paper, and for the first few files, it worked great. But as the project grew to 100+ endpoints, the IDE's "Loading..." spinner became a permanent fixture. Every time someone typed a dot to access a property, the entire editor would freeze for two seconds. We hadn't just written a bug; we'd created a type-level performance bottleneck that was choking the TypeScript compiler.

The Cost of Deep Recursion

TypeScript's type system is Turing complete, which is a fancy way of saying you can basically write programs that run inside the compiler. But just because you can doesn't mean you should. The most common culprit for slow type checking is deep recursion within conditional types. When you create a type that calls itself—like a DeepPartial or a recursive Flatten utility—the compiler has to keep track of every single iteration.

If your data structure is only three levels deep, you'll never notice. But if you're working with deeply nested JSON or complex tree structures, the compiler can enter a state of "combinatorial explosion." It spends an immense amount of CPU time trying to resolve whether a type matches a specific pattern across a dozen layers of nesting. I've seen cases where simplifying a recursive type to only go two levels deep reduced build times by several minutes.

// This looks clean, but can be slow on massive objects
type DeepReadonly<T> = {
  readonly [P in keyof T]: T[P] extends object 
    ? DeepReadonly<T[P]> 
    : T[P];
};

The Union Type Multiplication Effect

Another silent killer is the intersection of large unions. When you intersect two types, TypeScript tries to find all possible valid combinations. If you have a union of 20 possible types and you intersect it with another union of 20, you aren't just dealing with 40 types—you're potentially asking the compiler to evaluate 400 different combinations to ensure type safety.

You'll often see this happen when people try to build "exhaustive" event handlers or state machines. If you find yourself writing (TypeA | TypeB | TypeC...) & (StateA | StateB | StateC...), you're playing a dangerous game with your RAM. Instead of relying on massive intersections, I usually recommend using a discriminated union with a single type or kind property. It's much easier for the compiler to narrow down a single string literal than to solve a logic puzzle involving dozens of intersecting sets.

Overload Exhaustion and Type Inference

Finally, keep an eye on your function overloads. Overloads are great for API clarity, but the compiler checks them in order, from top to bottom. If you have a function with 15 different overloads and the one that actually matches your call is the 15th one, TypeScript has to fail 14 times before it finds the winner.

This becomes a real problem when those overloads involve generics. If the compiler has to perform complex type inference for each of those 14 failed attempts, your IDE will start to lag. If you find yourself writing a massive list of overloads, it's usually a sign that your function is trying to do too many different things. Breaking that function into three smaller, specialized functions doesn't just make the code cleaner—it makes the compiler's life significantly easier.




📋 Practical Task

Optimizing the Recursive Entity Mapper

You've inherited a codebase with a utility type called DeepSimplify. It's designed to flatten nested intersection types into a single object, but it's currently causing the IDE to lag because it's recursively traversing every single property of every object in the system, regardless of whether it needs to.

The Challenge: Modify the DeepSimplify type to prevent it from recursing into types that are already "simple" (like primitives, dates, or arrays), and implement a "depth limit" by adding a counter to the type to stop recursion after 3 levels. This will stop the compiler from spinning indefinitely on circular or deeply nested references.

type DeepSimplify<T, Depth extends number = 3> = 
  Depth extends 0 
    ? T 
    : T extends object 
      ? { [K in keyof T]: DeepSimplify<T[K], 
          // Your logic here: 
          // 1. Decrement the Depth counter
          // 2. Ensure we only recurse if T[K] is actually an object
        > } 
      : T;

// Test it with this deeply nested structure
interface User {
  id: string;
  profile: {
    bio: string;
    settings: {
      theme: string;
      notifications: {
        email: boolean;
        push: {
          enabled: boolean;
          frequency: string; // This is level 4, should be stopped by the limit
        }
      }
    }
  }
}

type SimplifiedUser = DeepSimplify<User>;
Rating
0 0

There are no comments for now.

to be the first to leave a comment.