Adopting the TypeScript 7 preview for typecheck only

Jesse James Richard
|
Aug 30, 2026
|
8 min read
#Method
#CI/CD
#Architecture
#Testing

The Go port of the TypeScript compiler ships as a preview, and its announcement promises full builds eight to twelve times faster. Whether a codebase can adopt it before the release settles depends on whether anything it ships is emitted by tsc. On Giant Context nothing is, and on 22 August the typecheck job across twenty-six packages moved to tsgo, the preview's binary. It went from seven and a half minutes to under five. That is about 35 percent rather than eight times, and the missing multiplier is in the orchestration rather than the engine.

That job runs on every push and twice before every production deploy, so those minutes sit on the critical path of every fix that reaches a customer's website. They are also what made deleting the test cache survivable, because a pipeline that runs cold every time can only afford it once its long jobs are shorter.

Whether anything shipped comes out of tsc

A compiler has two halves, the checker that decides whether code is well typed and the emitter that produces JavaScript and declaration files from it. The unfinished half of a preview compiler is emit. If tsc only checks, the preview's emitter never runs, and the risk of adopting it is bounded to two checkers disagreeing. Two checkers disagreeing shows up as a red job in CI rather than a changed artifact in production.

The build scripts answer this, not the documentation. Here every typecheck script is tsc --noEmit and every artifact comes out of something else. esbuild emits the services, vite the console, tsup the SDK and next the public website. A repository with tsc -b or tsc -p in a build script, or one that publishes declaration files straight from the compiler, should wait for the release.

The job being replaced ran on its own four-core runner with a 6144MB heap and a thirty-minute timeout, and took seven and a half minutes cold.

Two compilers in the lockfile

The preview publishes as @typescript/native-preview and installs the tsgo binary. The classic compiler stays installed as typescript, because anything that imports the compiler's API still needs it and the preview does not expose one. Three consumers here import it. The code generation in packages/generated rewrites source through ts-morph, a library over the compiler API. The typescript-eslint parser reads it. One code-style test imports the API directly. Those keep 5.9.3. Everything else moves.

The move is one word per package.

packages/core/package.json, before and after

// BEFORE   packages/core/package.json"typecheck": "tsc --noEmit"// AFTER"typecheck": "tsgo --noEmit"

baseUrl is a hard error in 7, and it was the one breaking change that touched this codebase. The plan was a hop through TypeScript 6.0 so its deprecation warnings would list every tsconfig still carrying it. grep found fourteen entries in twelve files. I planned a diagnostic tool to find work that grep could find.

Three consumers is a short list, and it is short because there is one language. Until June this backend was TypeScript and Python, and a two-language stack has two type checkers, two sets of pins and two of every migration. One compiler pin now governs every package in the tree, and a compiler swap is one commit instead of a project.

Where the multiplier goes in a monorepo

Eight to twelve times is a measurement of the engine on one project in one process. A monorepo typecheck has three costs, and only one of them is the engine. The task runner starts a process per package. The packages either run in parallel against a shared type graph or they run one at a time. And then the compiler runs. Which of the three owns the time decides what a faster compiler buys, and nothing in the announcement can say which one it is in someone else's tree.

Here the runner owned most of it. The runner was turbo, the tool that fans a script out across the packages of a monorepo, and there were twenty-six invocations. Each one was a turbo process starting a pnpm process starting the compiler. That startup was a fixed cost the engine could not touch, and removing the runner from the loop saved more than the engine did.

Turbo had also run the twenty-six three at a time. Three packages checking at once each reload the shared type graph, and on a four-core runner with a 6144MB heap that thrashes memory and finishes later than one process would. The loop that replaced turbo is sequential on purpose. Its header says why:

scripts/typecheck.sh

#!/usr/bin/env bash# Typecheck every package with tsgo (TypeScript 7 native compiler).# Replaces `turbo run typecheck` (26 per-package turbo/pnpm-exec invocations, whose# startup overhead swamped tsgo's engine speedup). Sequential on purpose: each package's# typecheck reloads the shared type graph, so running many tsgo processes at once thrashes# memory and is slower than one-at-a-time.set -uo pipefailcd "$(dirname "$0")/.."TSGO="$(pwd)/node_modules/.bin/tsgo"[ -x "$TSGO" ] || { echo "tsgo not found at $TSGO (run: pnpm install)"; exit 1; }fail=0for pj in packages/*/package.json; do  grep -q '"typecheck"' "$pj" || continue  d=$(dirname "$pj")  case "$d" in */storybook) continue ;; esac  out=$( cd "$d" && "$TSGO" --noEmit 2>&1 )  if printf '%s' "$out" | grep -q "error TS"; then    echo "### $d"    printf '%s\n' "$out"    fail=1  fidone[ "$fail" -eq 0 ] && echo "typecheck OK (tsgo, all packages)" || exit 1

That is the entire replacement for the runner. The CI job kept the same box, heap and timeout and lost its cache restore step, and the root typecheck script now points at this file.

The result is 35 percent cold, from trading three-way parallelism for single-process speed and dropping twenty-six process startups. The same three costs exist in any monorepo in a different ratio. A tree with five packages and one big one will see something close to the engine number. A tree with fifty small ones is paying for fifty startups and should measure the runner before measuring the compiler, because the runner is the cheaper fix and it may be most of the time.

What the two checkers will disagree about

The preview is a second implementation of the same language, and two implementations disagree on module resolution before they disagree anywhere else. The case here is a package that publishes both an ESM and a CommonJS type entry. The classic compiler dedupes them into one type. The preview resolves them as two unrelated types, and any generic constrained across the pair fails.

Any dependency with an exports map carrying separate import and require type conditions is a candidate, and the three named in the comment below all do. Running the loop above against a tree with the new binary produces the list in one pass, because every file that fails is a file the old compiler passed.

Here the list was three files totalling 294 lines in the shared table layer the console's lists are built on. All three type against Material React Table. A file like that has two answers. Keeping its package on the old compiler and branching the loop gives back some of the time for a slice of the tree. Turning checking off in the file with the condition written down gives back none of the time and all of the coverage. I took the second.

packages/ui/src/components/MRT/view.tsx, first lines

/** @format */// @ts-nocheck — tsgo (TypeScript 7 preview) mis-resolves Material-React-Table /// @mui / @tanstack esm-vs-main type entries as "two unrelated types," and resolves// MRT's generic row-key constraints differently than tsc. tsc passes this file clean.// Skipped from the tsgo gate until tsgo GA fixes package/type dedup.

Before this commit there were no @ts-nocheck directives in the repository. There are six now, and 294 lines of them are application code no type checker reads. That number does not appear in the job time and it is the price of the migration. Whichever answer a repository picks, the lines it turned off are the figure to write down next to the minutes it saved, because the minutes are on a dashboard and the lines are not.

The trade

The migration bought two minutes and forty-three seconds off every push and every deploy. It cost a second compiler in the lockfile, a sequential loop that will need revisiting when the tree grows, and 294 lines waiting on a release. On this repository, with one language, checker-only tsc, and a solo cadence of several pushes a day, that trade closes easily. On a repository that emits through tsc, or that still runs two languages, it does not.

Compute gets cheaper per request at scale

#War story
#Architecture
#AI & Agents

I price Giant Context at $25 per million tokens and had no defensible answer to what it costs to serve a thousand customers. Four of the five cost cla...

Jesse James Richard

|

Aug 30, 2026
Read previous

Building something like this

If this is the kind of engineering you need, get in touch.

Contact Jesse
Home
About
Contact
Sitemap
Privacy Policy
Terms of Service
Cookie Policy