Skip to content

Performance & Pitfalls

Type checking is real computation, and clever types can make tsc crawl or your editor lag. The type system is (famously) powerful enough to be a programming language of its own — and just like runtime code, it can be written in ways that are expensive to evaluate.

The usual culprits:

  • Huge unions. A union of thousands of string literals (often generated) multiplies work everywhere it’s used.
  • Deep recursion. Recursive conditional/mapped types that walk long tuples or strings hit exponential blow-ups and the instantiation-depth limit.
  • Distributive conditionals over big unions. A conditional type distributes over every member — over a large union that’s a lot of instantiations.
  • Over-eager inference. Very generic APIs force the checker to solve for many type arguments at every call.

You rarely notice until a file takes seconds to check or the editor’s red squiggles lag behind your typing. That’s the signal to look at your types, not just your code.

Beyond performance, these bite real teams:

  • Over-clever types nobody can read. A 40-line conditional type that saves three lines of duplication is usually a net loss. Cleverness is a cost paid by every future reader.
  • any leaks. One any spreads: any.foo is any, so it silently disables checking across everything it touches. Ban it with noImplicitAny and lint rules.
  • Casts that lie. x as Foo tells the compiler to stop checking — if x isn’t really a Foo, you’ve just hidden a bug. Double casts (x as unknown as Foo) are a giant red flag.
  • Enum surprises. Numeric enums allow any number as a value and emit runtime code; a const object with as const or a string-literal union is usually clearer and cheaper.
  • as overuse. Reaching for as to silence an error usually means you’ve stopped modeling the problem and started arguing with the compiler.
flowchart LR
  src["one any
(bad cast / JSON.parse)"] --> a1["any.user"]
  a1 --> a2["any.user.name"]
  a2 --> a3["passed to typed fn
checks silently skipped"]
How one any leaks through a codebase

When checks feel slow, measure instead of guessing:

  • tsc --extendedDiagnostics prints where time and memory go (check time, instantiation counts, memory).
  • tsc --generateTrace traceDir produces a trace you can open in a profiler to find the expensive types.
  • Bisect: comment out suspect types and watch the numbers move.

The fix is almost always simplify: replace a heroic conditional type with a plain union, break a giant type into smaller named pieces, or accept a slightly looser type that’s 10× cheaper and just as safe in practice.

Which of these commonly makes the type checker slow?
Why is a single `any` dangerous?
What does `x as Foo` actually do?
When a type is both clever and slow, what is usually the right move?