Why Your TypeScript Types Are Slowing Down Your Editor
You hover over a variable and wait a full second for the tooltip to render. Autocomplete in a large file feels like it is buffering. The "Find all references" spinner just keeps spinning. None of this is your machine being old. Most of the time it is your types asking the compiler to do an enormous amount of work on every keystroke.
The TypeScript language server runs the same type checker as tsc, just incrementally and under a tight latency budget. When a type is cheap to evaluate, the editor feels instant. When a type forces the checker to expand thousands of intermediate shapes, the editor feels broken. This article is about spotting the expensive patterns and rewriting them so your tooling stays snappy.
How the editor actually computes types
The language server is lazy. It does not eagerly resolve every type in your project; it resolves what it needs to answer the question you just asked, like "what is the type under my cursor" or "what completions are valid here." That laziness is why a project can have millions of lines of .d.ts and still feel fine.
The problem starts when a single type is structurally huge. Conditional types, large unions, and deep recursion all multiply the amount of work needed to answer one question. The checker also caches relationships between types, but pathological types blow past the cache and get recomputed. You can read the official guidance in the TypeScript performance wiki and related handbook pages, but the practical signal is simple: if hovering is slow, the type under your cursor is too big.
Union and conditional type explosion
The most common offender is a union that gets distributed across a conditional or mapped type. Distribution means the operation runs once per member, and members multiply fast when unions combine.
// A 50-member union times a 50-member union is 2,500 combinations.
type Pair<A, B> = `${A & string}-${B & string}`;
type Color = "red" | "green" | "blue" /* ...47 more */;
type Size = "xs" | "sm" | "md" /* ...47 more */;
// The checker materializes every combination as a literal string type.
type Variant = Pair<Color, Size>;Template literal types over large unions are a classic way to accidentally generate tens of thousands of string literals. Each one is a real type the checker has to hold in memory and compare. The fix is almost always to stop precomputing the cross product and validate at the boundary instead, keeping Color and Size as separate parameters rather than a fused literal.
A good rule of thumb: any type whose hover tooltip would be longer than your screen is a type the editor pays for repeatedly. If you cannot read it, the checker is still expanding it.
Deep recursion and "instantiation depth"
Recursive types are powerful for things like deep Partial or path strings, but each level of recursion is another instantiation the checker tracks. TypeScript caps recursion to protect itself, and hitting that cap surfaces as the error "Type instantiation is excessively deep and possibly infinite." Even when you stay under the cap, deep recursion is slow.
// Recurses through every nested key path of an object type.
type Paths<T> = T extends object
? { [K in keyof T]: `${K & string}` | `${K & string}.${Paths<T[K]> & string}` }[keyof T]
: never;On a small config object this is fine. Point it at a Redux store type or a generated GraphQL schema and the editor will crawl. If you need deep paths, bound the depth explicitly with a decrementing counter type, or generate the paths you actually use rather than every theoretical one.
Inference that has to guess too much
Sometimes the slowness is not one giant type but the checker repeatedly inferring complex generics because you never told it the answer. Library types built from long chains of infer, overloads, and intersections force the checker to try many candidates. The cure is to give it less to do.
// Slow: the checker re-derives the return type at every call site.
function build(config: Config) {
return /* a large inferred object */;
}
// Fast: an explicit return type lets the checker verify once and cache.
function build(config: Config): BuildResult {
return /* ... */;
}Adding explicit return types to exported functions is the single highest-leverage change for large codebases. It turns an open-ended inference problem into a quick assignability check, and it stops slow inference from rippling across every file that imports the function. The TypeScript handbook covers annotation versus inference if you want the deeper reasoning.
Measuring instead of guessing
You do not have to find these by intuition. The compiler can tell you where the time goes.
// In tsconfig.json, then run: tsc --noEmit
{
"compilerOptions": {
"generateTrace": "trace", // emits trace.json + types.json
"extendedDiagnostics": true // prints check time, memory, type counts
}
}Run tsc with these on and look at the type count and check time in the diagnostics output. A jump from tens of thousands of types to millions points straight at a distribution or recursion problem. The generateTrace flag produces files you can open in a trace viewer to see which file and which type ate the time. The flags are documented in the tsconfig reference. The editor uses the same checker, so a type that is slow in tsc is slow in your tooltip.
Quick wins that almost always help
- Add explicit return types to exported and public functions.
- Replace giant precomputed union types with runtime validation at boundaries.
- Bound recursive types with a depth limit instead of unbounded self-reference.
- Split barrel files so the editor loads fewer types per request.
- Prefer
interfacefor object shapes you extend; the checker caches them well.
None of these change your runtime behavior. They only change how much work the checker does to understand your code, which is exactly the work your editor is waiting on.
Takeaways
- Editor lag is usually type-checker lag; the language server runs the same checker as
tsc. - Large unions feeding conditional, mapped, or template literal types create combinatorial blowups.
- Deep recursive types are expensive even when they stay under the instantiation cap.
- Explicit return types convert slow open-ended inference into fast assignability checks.
- Measure with
extendedDiagnosticsandgenerateTracebefore you start rewriting types. - Every fix here is compile-time only, so your runtime stays identical while your editor gets fast again.

