TypeScript is nearly free at fifty thousand lines: the defaults work, the editor is instant, and the compiler is a background detail. Somewhere past a few hundred thousand lines it becomes infrastructure — with capacity limits, performance regressions, and failure modes — and it deserves the same deliberate engineering as your build or deploy pipeline. Teams that ignore this end up with ninety-second editor feedback, CI typechecks that dominate pipeline time, and a creeping culture of any-casts that quietly refunds the safety they adopted the language for.
The good news: the playbook for keeping TypeScript fast and honest at scale is well established. It has three pillars — strictness discipline, build architecture, and boundary integrity.
Strictness Is Cheapest Today
Every strict flag you defer gets more expensive with every merged PR. Greenfield packages should start with strict mode plus noUncheckedIndexedAccess and exactOptionalPropertyTypes — the flags that catch real production bugs like out-of-bounds reads and phantom undefined values.
For an existing lax codebase, big-bang conversion fails; ratchets work:
- ▸Per-package strictness in a monorepo: turn strict flags on package by package, starting with leaf libraries where the blast radius is small.
- ▸Error-count ratchets: snapshot the current error count under the stricter config and fail CI only when it grows; the number can only go down.
- ▸Ban new suppressions: a lint rule flags any newly added ts-ignore or as-any, while a burn-down list with named owners handles the existing ones.
Separate Transpilation From Typechecking
The single biggest architectural decision: stop asking one tsc invocation to both check your program and produce your JavaScript. Let esbuild, swc, or Vite handle transpilation — they strip types in milliseconds without checking them — and run tsc with noEmit as its own parallel CI lane and pre-merge gate. Developers get instant rebuilds; type errors still block merges. Two settings keep the split safe: isolatedModules guarantees every file can be transpiled independently, and verbatimModuleSyntax forces type-only imports to be explicit so nothing type-level leaks into runtime output.
Architect the Type Graph
At scale, how fast the checker runs is mostly a function of how you have shaped your dependency graph:
- ▸Project references per package: composite projects let tsc check each package against the emitted declaration files of its dependencies instead of re-analyzing their sources, and incremental builds skip whatever has not changed.
- ▸Explicit return types on package boundaries: inference across deep import chains is a major cost driver; annotating exported function signatures shrinks what the checker must recompute and makes declaration output faster and more stable.
- ▸Measure before optimizing: tsc offers extendedDiagnostics and a trace flag that show exactly where check time goes; the culprit is very often one clever mapped-conditional type in a core package, imported everywhere.
That last point deserves emphasis. Type-level programming is powerful, but a recursive conditional type in a hot package taxes every consumer's editor forever. Treat elaborate types like any other performance-sensitive code: justified by need, reviewed carefully, and owned by someone.
Types Must Be Honest at the Edges
The compiler proves internal consistency, not reality. Every byte entering the process — HTTP payloads, queue messages, environment variables, database rows — is untyped no matter what the annotation claims. Two practices close the gap. First, validate at I/O boundaries with zod or valibot, so the static type is derived from a schema that actually executed at runtime. Second, generate rather than hand-write types for external contracts: OpenAPI or GraphQL codegen for APIs, Prisma or Kysely for the database — hand-maintained mirror types drift silently, and always in the dangerous direction.
A practical sequence for a team starting the climb:
1. Turn on isolatedModules and move transpilation to esbuild or swc; keep tsc noEmit as the CI gate. 2. Adopt project references along your existing package boundaries and enable incremental builds. 3. Establish strictness ratchets and a suppression burn-down with named owners. 4. Add schema validation at every network and queue boundary, replacing as-casts as you go. 5. Profile check time quarterly and refactor the worst type-level hotspots like the performance bugs they are.
The return on this work is measured in trust and tempo. A fast, strict compiler is what makes a five-hundred-file rename a lunchtime task instead of a sprint, lets dependency upgrades land without archaeology, and gives every engineer — including the one hired last week — a machine-checked map of a system too large for any single head to hold. That is the actual product being bought: the ability to keep moving quickly in a codebase that has outgrown human memory.
