Performance & Pitfalls
Types have a compile-time cost
Section titled “Types have a compile-time cost”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.
The common footguns
Section titled “The common footguns”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.
anyleaks. Oneanyspreads:any.fooisany, so it silently disables checking across everything it touches. Ban it withnoImplicitAnyand lint rules.- Casts that lie.
x as Footells the compiler to stop checking — ifxisn’t really aFoo, 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; aconstobject withas constor a string-literal union is usually clearer and cheaper. asoveruse. Reaching forasto 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"]
Diagnosing it
Section titled “Diagnosing it”When checks feel slow, measure instead of guessing:
tsc --extendedDiagnosticsprints where time and memory go (check time, instantiation counts, memory).tsc --generateTrace traceDirproduces 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.