When I analysed 53 open-source repositories with Hotspots, a consistent difference emerged between TypeScript repos and the rest: exit_heavy is meaningfully lower in TypeScript (avg 4.5 across 28 repos) than in Go, Rust, Python, Java, and JavaScript (avg 5.3 across 25 repos). In the same TypeScript corpus, complex_branching has the highest max-out rate of any pattern — 13 of 28 repos reached the maximum count of 5, more than any other pattern.
These two facts together tell a consistent story. TypeScript code generates fewer exit-heavy functions than other languages do, but accumulates more functions with severe branching complexity. The cause is structural — it’s in how TypeScript’s type system shapes the code written against it — and it has a direct implication for which refactoring techniques are most useful.
The Data
Averaged across all repos analysed, the big-5 structural patterns look like this:
| Pattern | TypeScript avg (28 repos) | Non-TypeScript avg (25 repos) |
|---|---|---|
complex_branching | 4.0 | 4.6 |
exit_heavy | 4.5 | 5.3 |
god_function | 4.5 | 4.8 |
long_function | 4.6 | 4.8 |
deeply_nested | 3.6 | 3.9 |
Two things stand out. First, exit_heavy is meaningfully higher in non-TypeScript repos — an average of 5.3 vs 4.5. Second, complex_branching has the highest max-out rate within TypeScript: 13 of 28 repos hit a count of 5 or more, versus 11 of 28 for exit_heavy. In non-TypeScript repos, that ordering reverses — exit_heavy reaches count ≥ 5 in 18 of 25 repos, versus 13 of 25 for complex_branching.
Why TypeScript Generates More Branching
TypeScript’s type system encourages a pattern that other languages don’t: exhaustive dispatch over discriminated union types.
When a function receives a value typed as A | B | C, type-safe handling requires a switch or if/else chain where each arm narrows the type to one variant. The compiler enforces exhaustiveness. Each arm is correct, intentional, and necessary — but each arm also adds to the function’s cyclomatic complexity.
In colinhacks/zod, convertBaseSchema reaches CC 114 because it must handle every JSON schema variant: string, number, array, object, enum, anyOf, allOf, and so on. Each case is a branch. Zod’s _parse in v3 types reaches CC 64 for the same reason — every schema type gets its own parse path. In react-hook-form, createFormControl reaches CC 174 dispatching over field types, validation rules, and form states.
None of these are poorly written. They’re doing what TypeScript encourages: handling every case, explicitly, at the type level.
Why Go and Python Generate More Exits
Go’s idiomatic error handling creates a different pattern:
result, err := doSomething()
if err != nil {
return nil, err
}
A Go function with ten operations might have ten early returns — all for error propagation, not for branching logic. This inflates exit_heavy counts without proportionally raising cyclomatic complexity. The function isn’t making complex decisions; it’s propagating errors at each step.
Python’s guard-clause style produces the same effect: if not valid: return, if cache_hit: return cached — many exit points, modest branching. Rust’s ? operator is the same pattern made syntactic.
The result: in Go repos like schollz/croc, the top functions (Send, send, receive) have moderate CC values (8–15) but high exit_heavy counts because every operation that can fail adds a return path. In TypeScript, the exit count is lower because errors are typically thrown rather than returned, but branching is higher because type dispatch is structural.
What This Means for Refactoring
The practical difference matters because the techniques that reduce exit_heavy code don’t reduce complex_branching code, and vice versa.
For exit_heavy Go/Python code: consolidate error paths, use result objects, or pipeline the operations so errors propagate without explicit per-step handling. The goal is fewer return sites.
For complex_branching TypeScript code: extract each branch arm into a named handler function, then replace the switch/if chain with a dispatch map or strategy object. For discriminated unions, the pattern is:
// before: one large switch, CC climbs with every new type
function handle(event: EventA | EventB | EventC) {
switch (event.type) {
case 'A': /* 20 lines */ break;
case 'B': /* 20 lines */ break;
case 'C': /* 20 lines */ break;
}
}
// after: dispatch map, each handler independently testable
const handlers = {
A: handleEventA,
B: handleEventB,
C: handleEventC,
};
function handle(event: AppEvent) {
return handlers[event.type](event);
}
This preserves the exhaustiveness guarantee (you can enforce it with a mapped type on the handlers object) while collapsing the parent function’s CC to near 1 and making each handler independently testable.
The Repos That Show It Most Clearly
The TypeScript repos with the highest complex_branching counts share a common structural trait: they all process a large, well-defined type surface.
- react-hook-form —
createFormControl(CC 174): every field type, validation rule, and form state permutation handled in one place - zod —
convertBaseSchema(CC 114),_parse(CC 64): schema type dispatch, v3 and v4 - typeorm —
buildWhere(CC 52),preparePersistentValue(CC 39): SQL condition types and driver-specific type mappings - markedjs/marked — token type dispatch across the full Markdown AST
Each is a legitimate product of what the library does. The refactoring opportunity isn’t to eliminate the cases — it’s to stop handling all of them in one function.
This analysis is based on Hotspots runs against 53 open-source repositories between March and April 2026. Pattern counts reflect the top-5 hotspot functions per repo; the averages cited are across all repos in each language group.
Hotspots highlights structural and activity risk — not “bad code.” Editorial policy →