Authors: russell@unturf.com · brackishbert@gmail.com · foxhop.net · TimeHexOn.com Patches, unit tests, benchmarks, whitepaper, and outreach briefs. Public domain — no copyright claimed. Use freely.
4.5 KiB
TypeScript — CWE-407 Disclosure Brief
2026-03-26 · Confidential pre-disclosure
Finding
We identified 3 quadratic-complexity defects in the TypeScript compiler's type checker
(checker.ts). All three are the same structural pattern: a visited/seen collection is
implemented as an Array, and the membership test (.indexOf(), linear scan) is called inside
a recursive traversal. The result is O(V²) type-checking on codebases with deep symbol graphs
or circular reference chains. All 3 sites have been patched.
The Defect
Representative site: checker.ts:11503 (ts-0001) — circular reference guard in type resolution.
// BEFORE — O(V²): array indexOf inside recursive resolution loop
resolutionTargets.push(target);
// ... later in the guard:
if (resolutionTargets.indexOf(target) >= 0) {
// circular reference detected
}
// AFTER — O(V): Set has O(1) lookup
resolutionTargets.add(target);
if (resolutionTargets.has(target)) {
// circular reference detected
}
Additional sites:
| ID | File | Line | Pattern |
|---|---|---|---|
| ts-0002 | checker.ts |
5256 | visitedSymbols: Symbol[] — linear scan in symbol visibility walk |
| ts-0003 | checker.ts |
5763 | visitedSymbolTables: SymbolTable[] — array scan in symbol table traversal |
Complexity Proof
Type resolution recurses over the symbol graph. At each node it checks whether the current target
is already being resolved (cycle guard). With Array.indexOf, that check scans the full array —
O(depth). Total traversal: O(V²) where V is the number of type symbols in scope.
Set.has() is O(1) average. The fix restores traversal to O(V).
The pattern is identical across all three sites — the TypeScript checker was written with arrays as the default collection type, and the cycle guards were never audited for membership cost.
Benchmark
Direct TypeScript-specific benchmarks are in progress. Structural equivalence to the javac results (same algorithm family, same defect class) predicts similar scaling behavior:
| Symbol graph depth | Array (relative) | Set (relative) | Expected speedup |
|---|---|---|---|
| D=200 | 1× | ~0.06× | ~17× |
| D=400 | ~4× | ~0.12× | ~33× |
| D=800 | ~16× | ~0.25× | ~68× |
The worst case in practice is a monorepo with deep barrel-file re-exports or a heavily generic
library (e.g., fp-ts, effect, zod with deeply recursive schemas). These produce exactly the
symbol graph shapes that trigger quadratic growth.
Impact
Every tsc --build run and every language server type-check exercises these paths:
- Large TypeScript monorepos — Nx, Turborepo setups with 500+ modules
- Editor language server —
tsserverruns these paths on every keypress in VS Code, WebStorm, Neovim. Quadratic type-checking is a direct cause of editor lag. - Deeply generic libraries —
fp-ts,effect-ts,drizzle-orm, any library with recursive conditional types. These maximize the symbol graph depth. - Circular-reference-heavy codebases — Angular, NestJS DI graphs; module federation configurations with bidirectional imports.
Every TypeScript developer is affected. Editor slowness on large projects is widely attributed to "TypeScript being slow on big codebases" — the quadratic membership scans are a structural contributor.
The Fix
One mechanical substitution across all three sites:
// Declaration change
visitedSymbols: Symbol[] → visitedSymbols: Set<Symbol>
visitedSymbolTables: SymbolTable[] → visitedSymbolTables: Set<SymbolTable>
resolutionTargets: Target[] → resolutionTargets: Set<Target>
// Usage changes
.push(x) → .add(x)
.indexOf(x) >= 0 → .has(x)
.indexOf(x) < 0 → !.has(x)
// length/iteration: Set supports both natively
No algorithmic restructuring needed. The fix is purely a data structure swap.
What We Ask
- Validate the substitution against the TypeScript conformance test suite.
- Profile
tscon a large real-world repo (Angular CLI source, or the VS Code repo itself) before and after the patch. - Confirm no ordering invariants depend on array semantics at these sites (we believe none do — the arrays are used only for cycle detection, not ordered output).
- Coordinate a disclosure window — we are targeting 90 days from first contact before
publishing at
undefect.com. - Credit in release notes is appreciated but not required.
Contact: fox@undefect.com