Skip to Content
Course content

88: Type Checking Performance Profiling

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

We've all been there: your project grows, your types get more sophisticated, and suddenly, your IDE starts lagging. You hit "Save," and there's a three-second pause before the red squiggles actually update. When this happens, the natural instinct is to complain that "TypeScript is slow" or to assume your machine just needs more RAM. But the truth is, TypeScript isn't usually slow because of the amount of code you have—it's slow because of the complexity of specific type calculations.

The Guesswork Game

The naive way to handle a slow build is "guess-and-check." You look at your most complex file—maybe the one with that massive 500-line union type or the recursive utility that maps your API responses—and you comment it out. If the build time drops, you've found the culprit. If not, you move to the next file. This is a waste of your time and, frankly, a bit desperate. You're treating the compiler like a black box, hoping that your intuition about which type is "expensive" matches the reality of how the TypeScript checker actually works.

I've seen developers spend hours refactoring a large interface thinking it was the bottleneck, only to find out that the real killer was a single, deeply nested conditional type used in a completely different utility file. The compiler doesn't care about the number of lines; it cares about the number of checks it has to perform. A small, clever recursive type can easily trigger thousands of internal checks, bringing your productivity to a grinding halt.

Letting the Compiler Tell You the Truth

Instead of guessing, you should be using the built-in profiling tools. TypeScript has a flag called --generateTrace that essentially records everything the type checker does and dumps it into a directory. It's not a pretty log file; it's a trace event file that you can load into a visualizer.

# Run the compiler and output a trace to a folder
npx tsc --generateTrace traceDir ./ts-trace

Once you have those files, you can drop them into the TypeScript Trace Viewer (available online or as a local tool). This gives you a Gantt-chart view of exactly which files and which specific types took the longest to resolve. Instead of wondering if your DeepMerge utility is the problem, you can see a literal bar representing 400ms of execution time spent on one specific line of code. Now you're not guessing; you're engineering.

The Hidden Cost of Recursive Conditionals

When you dive into those traces, you'll often find that the bottleneck is "Recursive Conditional Types." Let's look at a common example: a type that recursively converts all keys in an object to uppercase. It looks elegant, but it's a performance trap.

type UpperCaseKeys<T> = T extends object 
  ? { [K in keyof T as Uppercase<string & K>]: UpperCaseKeys<T[K]> } 
  : T;

On a shallow object, this is fine. But if you're applying this to a deeply nested state tree or a complex library type, TypeScript has to recursively enter every single branch. If you have nested objects with circular references or just extreme depth, the compiler starts to struggle. The trade-off here is between "perfect type safety for every single leaf node" and "a compiler that actually finishes in under ten seconds."

The fix is usually to simplify the type or introduce a depth limit. Sometimes, the most "professional" thing you can do is admit that a specific piece of the type system is too complex for the compiler to handle efficiently and strategically use any or unknown at the deepest level of the recursion. It feels like cheating, but when it turns a 15-second check into a 2-second check, your whole team will thank you.




📋 Practical Task

Profiling and Optimizing a Recursive API Mapper

You have been handed a legacy codebase with a utility called DeepReadonly<T> that is causing significant IDE lag in a project with deeply nested configuration objects. Your goal is to identify the performance bottleneck and optimize it.

The Setup:

  1. Create a file named perf-test.ts.
  2. Implement a recursive DeepReadonly<T> type that makes every property of an object, and every property of its children, readonly.
  3. Create a mock "Config" object that is nested 10 levels deep (e.g., { a: { b: { c: ... } } }).
  4. Apply DeepReadonly to this object in several different variables.

The Challenge:

  1. Run npx tsc --noEmit --generateTrace traceDir ./trace.
  2. Open the resulting files in the TypeScript Trace Viewer.
  3. Locate the checkType or calculateType operations that are taking the most time.
  4. Refactor the DeepReadonly type to avoid unnecessary recursion (for example, by excluding certain types like Date, RegExp, or Map from being recursed into) and verify if the trace duration for that operation decreases.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.