In TypeScript, why can type-only helpers slow `tsc` and editor IntelliSense down noticeably, and which kinds of types tend to be the expensive ones?
answer
- work per instantiation, work per comparison
- the editor recomputes on every keystroke
- big unions multiply comparisons
- annotate exported return types
- named types hit the relation cache
basics
~20 sThe checker does real work per generic instantiation and per assignability comparison, and the editor's TypeScript server repeats it while you type. Large unions, deeply nested conditionals, long intersection chains and inferred-instead-of-annotated return types are the usual expensive shapes.
solid answer
~50 sTypes are erased, but *checking* them is not free. Each use of a generic creates a fresh instantiation, each conditional type is evaluated per set of arguments, and each mapped type materialises a property per key — so a helper's cost multiplies by how many places use it. Assignability checks then compare those structures, and a union forces a comparison against many constituents rather than one type. The expensive shapes are big unions and union cross-products, deeply nested or recursive conditionals, long intersection chains, and large anonymous object types that the checker has to re-expand instead of matching by name. The single highest-leverage fix is annotating exported functions' return types, so the checker stops re-inferring and re-expanding a large type at every call site; after that, prefer named interfaces over sprawling intersections, and split monster helpers into named intermediate aliases. And remember the editor pays this cost on every keystroke, not once per build.
code
typescript · 16 linesinterface Handlers {
onSave(): void;
onCancel(): void;
}
// Explicit return type: the checker uses `Handlers` directly at every call site.
export function makeHandlers(save: () => void): Handlers {
return { onSave: save, onCancel: () => {} };
}
// Prefer this over `type Row = A & B & C` for shapes compared everywhere.
interface A { id: string }
interface B { createdAt: number }
interface Row extends A, B {
label: string;
}go deeper
Know that although types disappear from the output, the compiler still has to check them, so a complicated type costs build time and can make your editor feel slow.
Explain the two cost drivers concretely — one instantiation per use of a generic, and structural comparison on every assignability check — and name expensive shapes such as huge unions and long intersection chains.
Demonstrate the fixes you would actually apply under time pressure: annotate exported return types, split helpers into named aliases, constrain parameters, and verify the improvement with a measurement rather than a hunch.
Own the tradeoff at codebase scale: set expectations for where reusable generic utilities earn their check-time cost, and treat developer editor latency as a first-class budget alongside CI duration.
## Why erased types still cost something TypeScript emits JavaScript with the type layer deleted, so nothing here is a runtime concern. But before the compiler can delete the types, it has to *check* them, and that checking is ordinary computation with ordinary costs. Two operations dominate. **Instantiation.** A generic type is a template. Every time you use it with concrete arguments, the checker builds a new type from that template. `Pick<User, "id">` and `Pick<Order, "id">` are two instantiations. A helper defined once and used in 300 places is instantiated 300 times, and if that helper internally instantiates three others, the multiplication compounds. Conditional types add another dimension: the checker must evaluate the condition for each argument set, and the true/false branches may themselves instantiate more types. Mapped types materialise one property per key of the source, so mapping a type with 200 properties costs 200 properties of work per instantiation. **Relation checking.** Every assignment, argument pass and return is an assignability question: is `A` assignable to `B`? For two object types this is a structural walk over members. The checker caches relation results, which is why a codebase full of *named*, reused types is cheaper than one full of freshly computed anonymous types — a cache hit ends the walk immediately, while a newly instantiated anonymous shape has to be compared from scratch. ## The shapes that hurt - **Very large unions.** Checking against a union frequently means checking against its constituents. A union of tens of thousands of members — easy to produce accidentally with template-literal cross-products or by distributing a conditional over another big union — turns each comparison into a lot of comparisons. At the extreme the compiler refuses outright with "Expression produces a union type that is too complex to represent." - **Deeply nested and recursive conditionals.** Each level is another evaluation, and a recursive helper's cost grows with the depth of the data it is applied to. - **Long intersection chains.** `A & B & C & D & …` gives the checker a pile of members to reconcile at every comparison. The TypeScript team's own performance guidance is to prefer `interface X extends A, B` over the equivalent intersection: an interface is a single named, cached, flattened declaration, and the compiler can also report conflicts at the declaration instead of at every use site. - **Large inferred types crossing module boundaries.** If an exported function has no return type annotation, the checker infers it — potentially a big anonymous structure — and then has to re-expand and re-compare that structure wherever the function is called, and again when writing declaration files. ## Why the editor suffers first `tsc` pays these costs once per build. Your editor does not run `tsc`; it runs the TypeScript *server*, which uses the same checker to power hovers, completions, go-to-definition and inline errors, and it re-answers those questions continuously as you edit. That means an expensive helper is paid over and over, per keystroke, by every developer touching a file that uses it. This is why "autocomplete got slow in this file" is usually the first symptom of type-level work that has outgrown its value — long before CI time becomes anyone's complaint. ## What actually helps **Annotate return types on exported functions.** This is the highest-leverage change and it is the compiler team's standard recommendation. An explicit annotation lets the checker use the written type directly instead of inferring a large one and re-expanding it at each call site. ```ts interface Handlers { onSave(): void; onCancel(): void; } // The annotation stops the checker re-deriving a large anonymous type per call site. export function makeHandlers(save: () => void): Handlers { return { onSave: save, onCancel: () => {} }; } ``` **Prefer named interfaces to sprawling intersections** for shapes that are compared often. **Break a monster helper into named intermediate aliases**, which both improves cache behaviour and makes the failure message readable. **Constrain type parameters** so the checker has less to explore, and avoid unbounded recursion over arbitrary depth. **Measure rather than guess.** The cost is rarely where intuition says it is, and a five-minute measurement beats an afternoon of speculative rewriting. ## The judgment part An interviewer asking this wants to hear that you know the cost is real and where it lands, without concluding that generics are bad. Reused generic utilities are how a codebase stays DRY; the problem is specifically the *elaborate* helper whose guarantee could have been expressed as a plain explicit type. Cost per unit of guarantee is the right metric, and the editor — not CI — is where the bill arrives.
- Why do the TypeScript maintainers recommend an interface extending several types over the equivalent intersection?An interface is one named declaration: its members are resolved once and it participates in the checker's relation cache by identity, so repeated comparisons are cheap. An intersection is recomputed as a set of constituents at each comparison, and conflicts surface at every use site instead of once at the declaration.
- Why does adding an explicit return type to an exported function speed up checking?Without one, the checker infers the return type, which may be a large anonymous structure, then re-expands and re-compares it wherever the function is used and again when emitting declarations. An annotation gives it a written, usually named type to use directly, cutting all that repeated work.
- Does turning on `skipLibCheck` help with a slow project?It can, but only for cost coming from checking dependency `.d.ts` files — it skips type checking of declaration files. It does nothing about expensive helpers in your own source, so measure first: if the counts point at your own instantiations, `skipLibCheck` will not move the number.
- Is any of this affected by the bundler you use?No. Bundlers either strip types without checking them or delegate checking to the compiler; the instantiation and comparison work is the checker's, wherever it is invoked from. Moving to a faster transpiler makes builds faster by skipping checking, not by making expensive types cheap.
saying these in an interview costs you the question
- Believes generics cost something at run time
- Thinks a faster bundler makes expensive types cheap
- Assumes CI build time is where the cost is first felt
- Says intersections and interfaces are interchangeable in every respect
- Claims type-check cost scales only with lines of code