The State of TypeScript CI 2026
Where tsc actually spends its wall-clock in continuous integration: type-checking versus emit, the scaling wall large monorepos hit, and what project references and incremental builds return when the cache survives.
Executive summary
TypeScript adoption is effectively universal in professional JavaScript, and with it comes a CI cost most teams underprice: the type-check. Modern transpilers emit JavaScript in seconds by skipping the type system entirely, which has trained a generation of teams to think of their TypeScript build as fast. But the emit was never the slow part. Teams still run a separate tsc pass purely to type-check, and that pass, not the emit, is what dominates the clock and gates the merge as a codebase grows.
The type-checker scales with the size and connectivity of the type graph, not just the number of files, so it grows faster than intuition expects. Shared types, generics, and inferred unions multiply the work the checker has to do, which is why a codebase that doubles in files can more than double in check time. Project references and incremental builds exist precisely to bound that growth by only re-checking what changed, but they only pay off when the prior build state is present, and in CI that state is routinely thrown away on every job.
This is the central tension of TypeScript in CI in 2026. The compiler ships excellent mechanisms for making the type-check fast, and ephemeral CI runners systematically defeat them by starting cold every time. A clean checkout has no .tsbuildinfo to read, no prior project-reference outputs to reuse, and so it re-checks the entire world on every push, paying full cold-check cost forever. The mechanisms are there; the runner model discards them.
Three numbers frame the year. About sixty-three percent of a cold tsc run is spent type-checking rather than emitting, which is why a faster transpiler does nothing for the pass that actually gates merges. A warm incremental build type-checks roughly 4.7 times faster than a cold one, which is the size of the prize sitting unclaimed on every ephemeral runner. And over nine in ten professional JavaScript developers use TypeScript, which means this is not a niche cost but a tax on nearly every modern web pipeline.
The path to claiming that prize is structural plus operational. Project references bound the blast radius of a change so the checker re-validates only the affected projects, incremental compilation reuses the prior check so warm runs are deltas rather than full passes, and a runner that persists the build state between jobs turns the warm case from an accident into the default. This report quantifies the type-check tax, what each mechanism returns when warm, and why the runner layer is what decides whether a team pays cold-check cost on every merge or warm-check cost the way the compiler intended.
Estimated split of a cold full type-check-and-emit pass on a mid-size codebase. · Source: Latchkey analysis (modeled)
Wall-clock for tsc on a mid-size codebase across reuse strategies. · Source: Latchkey analysis (modeled)
Email me the report
The full report is right here on this page, free. Want the link in your inbox to read later or share, plus new Latchkey reports as they drop? Drop your email and we will send it over.
Sent! Check your inbox for the report link.
No spam. Unsubscribe anytime.
Type-checking is the cost, not transpiling
Teams that adopt a fast transpiler for emit often assume their TypeScript build is fast, and for the emit itself they are right. Stripping types and writing out JavaScript is mechanical and quick, and the tools that do only that finish in seconds even on large codebases. The mistake is concluding that the TypeScript build as a whole is therefore fast, because emit was never the expensive part of what gates a merge.
On a cold run, type-checking dominates tsc wall-clock. In the modeled split roughly sixty-three percent of the time is the type-check, with program and binder setup and module resolution accounting for most of the rest, and emit a small slice at the bottom. The checker is doing genuinely hard work: resolving types across the whole program, evaluating generics, and verifying every assignment against the type graph, none of which a transpiler does at all.
The implication is sharp. Swapping in a faster transpiler speeds up the part that was already cheap and does nothing for the pass that actually fails the build when a type is wrong. A team that wants its TypeScript CI to be fast has to make the type-check itself incremental, because that is where the minutes are. The donut below makes the proportion concrete, and it is the reason the rest of this report is about the type-check rather than the emit.
The type-check scales with the type graph, not the file count
tsc time grows with the size and connectivity of the type graph, which expands faster than the raw file count as a codebase matures. Shared types get imported in more places, generics get instantiated against more arguments, and inferred unions widen as more call sites contribute to them. The checker's work is a function of all those relationships, not just the number of source files, so the cost curve is steeper than a per-file model would predict.
The modeled scaling makes the wall concrete. A thousand-file codebase checks in roughly twenty-two seconds, five thousand files in about seventy-eight, fifteen thousand in around three and a half minutes, and forty thousand files in roughly nine minutes for a single cold pass. The jumps are super-linear: each tier is more than proportionally slower than the last, because the type graph is denser as well as larger.
This is why large monorepos hit a type-check wall that smaller projects never feel. A small service can re-check the world on every push indefinitely and never notice, but past a certain size a full cold check becomes a multi-minute gate on every merge, and the only durable fix is to stop re-checking the whole graph each time. Bounding the work per run is not an optimization at that scale, it is the difference between a usable pipeline and an unusable one.
Modeled tsc wall-clock as the type graph grows with file count. · Source: Latchkey analysis (modeled)
Project references bound the blast radius
Project references split the program into independently checkable units with declared dependencies between them. A change in one project forces a re-check of that project and the projects that depend on it, but not the whole graph, so the work scales with the affected slice rather than with the entire codebase. They are the structural fix for the type-check scaling problem, and for a large monorepo they are close to mandatory.
The catch is that references only deliver in CI when the prior build outputs are present. The mechanism works by reading the previously emitted declaration files and build metadata of upstream projects and skipping anything that has not changed. On a clean ephemeral checkout none of that prior state exists, so the references are cold and every project gets re-checked from scratch. The structural win is real, but it is contingent on the build state surviving between runs.
The chart of cold type-check times shows references helping even cold, because the binder and resolution work can be partitioned, but the large prize is the warm case where unchanged projects are skipped entirely. The lesson for teams is that adopting project references is necessary but not sufficient: without a way to carry the prior outputs into the next CI run, you have paid the structural cost of splitting the program and are still paying cold-check time on every push.
- References split the program so a change re-checks only the affected projects and their dependents, not the whole graph.
- They are close to mandatory for large monorepos, where a full cold check is a multi-minute gate.
- On a clean ephemeral checkout the references are cold, so the savings only appear when prior outputs survive between runs.
Incremental builds need .tsbuildinfo to survive between runs
Incremental compilation stores a .tsbuildinfo fingerprint of the last check and reuses it to skip unchanged work on the next run, turning a full pass into a delta. In the model a warm incremental check runs roughly 4.7 times faster than a cold one, dropping from around 142 seconds to about 47, because the checker only re-validates what actually changed since the recorded state.
The entire mechanism depends on that one file surviving between runs. .tsbuildinfo is the memory of the previous check, and without it incremental mode has nothing to be incremental against and falls back to a full cold pass. This is exactly where default ephemeral CI defeats the compiler: a fresh runner per job discards the file every time, so the pipeline pays cold-check cost on every push no matter how carefully incremental mode is configured.
The fix is to treat the build state as something to persist, not something to regenerate. Caching .tsbuildinfo and the project-reference outputs on a warm shared volume, or running on a runner that keeps that state between jobs, is what converts the configured-but-cold incremental setup into an actually-warm one. The chart below traces the path from a cold full check down to the warmest case, and the gap between the first bar and the last is the standing prize that ephemeral runners leave on the table every single run.
- .tsbuildinfo is the recorded fingerprint of the last check; incremental mode is a full cold pass without it.
- Default ephemeral runners discard it every job, so CI pays cold-check cost on every push despite correct configuration.
- Persisting it on a warm volume, or running where state survives, is what unlocks the modeled 4.7x warm speedup.
The cost of the type-check is real money, run after run
Wall-clock time is the visible cost, but the type-check is also a recurring line item on the CI bill, and the runner path decides how large it is. The modeled cost of a single CI type-check is about nineteen cents on a hosted runner running cold, dropping to roughly eight cents on a hosted runner with a warm incremental build, six cents self-hosted and warm, and around three cents on a managed runner with persisted cache.
Multiplied across every push, every pull-request update, and every branch, those per-run cents become a meaningful and entirely recurring expense, and the cold-versus-warm gap is the largest single lever on it. A team paying nineteen cents a run because its runner discards the build state is paying more than six times the warm-managed cost for the same check, on every merge, indefinitely.
The cost chart and the time chart tell the same story from two angles: warming the build state both shortens the check and cheapens it, because a shorter check bills fewer runner-minutes. There is no trade-off to balance here. The warm case is faster and cheaper at the same time, which is what makes the persisted-state question the central one for any team that type-checks TypeScript in CI at scale.
Modeled cost of one CI type-check across runner choices, cold vs warm. · Source: GitHub Actions pricing + Latchkey rates
Large tsc runs are prone to out-of-memory failures
A full cold type-check of a large type graph is memory-hungry, and on an undersized or default runner a big tsc run can be killed by the operating system before it finishes. The failure looks like a broken build, but the type-check was fine: the environment simply ran out of headroom partway through, often non-deterministically depending on what else the runner was doing.
This category of failure is especially painful for cold checks, because the run that gets OOM-killed is also the most expensive one to repeat. A team without a warm build state pays full cold-check time, sometimes hits an out-of-memory kill near the end of it, and then pays full cold-check time again on the retry, turning one logical check into two full passes worth of minutes.
The durable answers are right-sizing the runner so the check has the memory headroom it needs and warming the build state so the check is a small delta rather than a memory-intensive full pass in the first place. A delta check touches far less of the type graph and is far less likely to exhaust memory, which is another way the warm case pays off: it is not just faster and cheaper but also more reliable.
A managed runner makes the warm case the default
The fastest type-check in this report combines warm project references with a persisted incremental cache on a managed runner, landing at around thirty seconds where the cold full check took about 142. That is not a different compiler or a different codebase, it is the same tsc with its prior state carried into the run instead of thrown away, which is exactly what the compiler's incremental and reference machinery was designed to exploit.
The compute also lands around 70 percent cheaper than hosted rates, so the warm case wins on cost at the same time it wins on speed. Because the runner persists .tsbuildinfo and the reference outputs between jobs, the warm path is the default rather than a lucky cache hit, which is the structural change that converts the compiler's mechanisms from theoretically available into reliably realized.
Auto-healing closes the last gap. Large tsc runs are prone to transient out-of-memory failures, and a runner that retries a mechanically failed check on a fresh environment absorbs those without a human ever seeing a red build, and without forcing a full cold re-check. Together, persisted state and automatic recovery make the warm, fast, cheap, reliable type-check the normal outcome of a CI run rather than the exception a team occasionally gets when the cache happens to survive.
- Warm refs plus a persisted incremental cache cut the modeled cold check from ~142s to ~30s on the same compiler.
- Managed compute lands roughly 70% below hosted rates, so the warm case wins on speed and cost together.
- Auto-healing retries a transient OOM on a fresh environment, so a mechanical failure does not force a full cold re-check.
Recommendations
Stop optimizing emit and make the type-check incremental
A faster transpiler speeds up the cheap part and leaves the pass that gates merges untouched, because type-checking is the majority of tsc wall-clock. Direct the effort at making the type-check itself a delta rather than a full pass. That is where the minutes are, and it is the only change that moves the number developers actually wait on.
Adopt project references before the monorepo hits the wall
References bound the blast radius of a change so the checker re-validates only the affected projects and their dependents, not the whole graph. For a large or growing monorepo they are close to mandatory, because a full cold check becomes a multi-minute gate past a certain size. Adopt them early, while the program is still easy to partition.
Persist .tsbuildinfo and reference outputs between CI runs
Incremental mode and project references both depend on prior build state, and default ephemeral runners discard it every job, so CI pays cold-check cost on every push despite correct configuration. Cache the build state on a warm shared volume or run where it survives between jobs. This single change unlocks the modeled 4.7x warm speedup that is otherwise left on the table.
Right-size the runner so large checks do not OOM
A full cold type-check of a large type graph is memory-hungry and can be killed mid-run on an undersized runner, turning one check into two full passes worth of minutes on the retry. Give the check enough memory headroom, and warm the build state so it is a small delta that is far less likely to exhaust memory in the first place.
Measure cost per type-check, not just wall-clock
The type-check is a recurring bill, and the cold-versus-warm gap is the largest lever on it: a cold hosted run can cost more than six times a warm managed one for the same check, on every merge. Track cost per run alongside duration so the finance and engineering views agree, and so the savings from warming the build state are visible where budgets are set.
Outlook
Expect the structural fixes to keep being adopted while the operational gap that defeats them persists. Project references and incremental compilation are well understood and increasingly default in large TypeScript codebases, but the ephemeral-runner model that discards their state on every job is just as entrenched, so the common case through 2026 will remain teams that have configured the fast path and are still paying cold-check cost because nothing carries the build state forward.
The compiler ecosystem is moving toward faster type-checking at the engine level, and that will lower the absolute numbers across the board. But it does not change the shape of the problem: a faster cold check is still a full cold check, and the multiple between cold and warm stays roughly the same. The teams that benefit most from a faster checker will still be the ones that also warm the build state, because the two gains compound rather than substitute.
For most teams the practical takeaway is that TypeScript CI speed is decided less by the compiler version and more by whether the runner remembers the last check. The mechanisms to make the type-check fast already ship in the box. The work is operational: persist the build state, right-size the runner, and recover from the transient out-of-memory failures that large checks invite, so the warm, fast, cheap type-check the compiler is capable of becomes the one the team actually gets on every push.
Methodology
This report combines published TypeScript compiler documentation and JavaScript ecosystem data with Latchkey's own analysis of CI runner economics. Type-check timings are modeled on representative codebase sizes and will vary with type-graph complexity, strictness settings, and hardware, so the figures describe direction and magnitude rather than a precise per-codebase value. Cost figures derive from published GitHub-hosted per-minute rates and Latchkey managed rates. Figures labeled "modeled" are illustrative estimates derived from public pricing and typical pipeline shapes, not a primary survey; figures attributed to a named source reflect that source. Pricing reflects published rates at time of writing and should be verified against current provider pricing.
Sources
- JetBrains Developer Ecosystem Survey
- Stack Overflow Developer Survey
- GitHub Actions - billing & pricing
- GitHub - Octoverse