TypeScript produces less exit-heavy code than Go or Python — and more branching

Across 53 OSS repos, exit_heavy averages 5.3 in Go/Python/Rust but only 4.5 in TypeScript — while complex_branching has the highest max-out rate of any pattern in TS. The cause is structural: discriminated unions and type narrowing create high-CC functions that other languages handle differently.

Stephen Collins ·
editorial typescript code-health refactoring data

Key Points

What is complex_branching and why does it max out more often in TypeScript?

complex_branching flags functions with high cyclomatic complexity driven by conditional logic — if/else chains, switch statements, and type-narrowing branches. TypeScript's type system encourages this pattern: discriminated unions require exhaustive switch statements to be type-safe, and type narrowing via instanceof, typeof, and in checks produce structurally complex functions that are nonetheless correct TypeScript. The result is that complex_branching reaches its most severe form more often in TypeScript than in other languages — 13 of 28 TypeScript repos maxed it out, versus 13 of 25 non-TypeScript repos — while exit_heavy is both more prevalent and more severe outside TypeScript.

What is exit_heavy and why does it dominate in Go and Python?

exit_heavy flags functions with many return points — guard clauses, early exits, and error returns distributed throughout the function body. Go's idiomatic error handling (if err != nil { return err }) and Python's early-return style produce many exit points without increasing cyclomatic complexity as dramatically. A Go function can have eight return statements and still have a CC of 8, while a TypeScript function dispatching over a union type can reach CC 50 with no premature returns at all.

Which TypeScript repos show complex_branching most severely?

react-hook-form (createFormControl: CC 174), zod (convertBaseSchema: CC 114, _parse: CC 64), typeorm (buildWhere: CC 52), and tldraw all show complex_branching at the maximum count. What they share is a large discriminated type surface — form field types, schema types, query condition types — that must be handled exhaustively.

How do I reduce complex_branching in TypeScript?

The most effective technique is to extract each branch arm into a dedicated function and use a lookup map or strategy object to dispatch between them. For discriminated union switches, extracting each case handler into a named function preserves type narrowing while collapsing the parent function's cyclomatic complexity. The goal is to replace a large switch body with a single dispatch that delegates to small, independently testable handlers.

Does this mean TypeScript code is worse than Go or Python?

No — it means the dominant structural risk differs by language, and the refactoring techniques that work in Go (flattening error-return chains) don't address TypeScript's structural problem. TypeScript's branching complexity often reflects deliberate, type-safe dispatch over union types. The issue is scope creep: these dispatch functions accumulate more and more cases over time, and the right intervention is decomposition by type group, not early-return flattening.

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:

PatternTypeScript avg (28 repos)Non-TypeScript avg (25 repos)
complex_branching4.04.6
exit_heavy4.55.3
god_function4.54.8
long_function4.64.8
deeply_nested3.63.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-formcreateFormControl (CC 174): every field type, validation rule, and form state permutation handled in one place
  • zodconvertBaseSchema (CC 114), _parse (CC 64): schema type dispatch, v3 and v4
  • typeormbuildWhere (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 →