From 5bb27eacc34fa7dbd19641d2d0975b76c0120cc1 Mon Sep 17 00:00:00 2001 From: Shahid Raza Date: Sun, 12 Jul 2026 13:54:44 +0530 Subject: [PATCH] feat(threads): add two TypeScript 7 threads Covers the Go rewrite (why shared-memory concurrency was the missing capability, why not Rust) and the migration reality (removed options, changed defaults, the missing programmatic API). Co-Authored-By: Claude Opus 4.8 --- .../threads/upgrading-to-typescript-7.mdx | 127 ++++++++++++++ .../why-typescript-7-is-written-in-go.mdx | 156 ++++++++++++++++++ src/lib/thread-covers.ts | 4 + 3 files changed, 287 insertions(+) create mode 100644 src/content/threads/upgrading-to-typescript-7.mdx create mode 100644 src/content/threads/why-typescript-7-is-written-in-go.mdx diff --git a/src/content/threads/upgrading-to-typescript-7.mdx b/src/content/threads/upgrading-to-typescript-7.mdx new file mode 100644 index 0000000..e6f7e99 --- /dev/null +++ b/src/content/threads/upgrading-to-typescript-7.mdx @@ -0,0 +1,127 @@ +--- +title: "Upgrading to TypeScript 7: what actually breaks" +description: "A field guide to the TypeScript 7 migration: the compiler options that are now hard errors, the missing programmatic API, and why a Next.js + MDX site like this one still needs TypeScript 6 in the editor." +date: 2026-07-12 +tags: ["typescript", "tooling", "migration"] +--- + +[TypeScript 7 is 8-12x faster](/threads/why-typescript-7-is-written-in-go), which makes the +upgrade tempting to do on a Friday afternoon. Don't. The speed is real, but 7.0 is the +release where the team cashed in every deprecation they've been sitting on *and* shipped +without a programmatic API. Whether you can move depends less on your code than on your +toolchain. + +Here's the honest checklist, in the order the breakage actually hits you. + +## 1. Options that are now hard errors + +TypeScript 6 made these warnings. TypeScript 7 makes them errors, so `tsc` refuses to start +rather than silently doing something else. Removed outright: + +| Removed | What to do instead | +| --- | --- | +| `target: es5` | Target a modern runtime; downlevel in your bundler if you truly must | +| `downlevelIteration` | Gone with ES5 | +| `moduleResolution: node` / `node10` | `nodenext` (Node) or `bundler` (Vite, webpack, Next) | +| `module: amd` / `umd` / `systemjs` / `none` | `esnext` | +| `baseUrl` | `paths`, resolved relative to the project root | +| `esModuleInterop: false`, `allowSyntheticDefaultImports: false` | Cannot be disabled; delete them | + +If you're on a modern setup, most of this is a no-op. If you're carrying a `tsconfig.json` +that has been copy-pasted since 2019, this is where your afternoon goes. + +## 2. Defaults that changed under you + +More dangerous than the removals, because these don't error, they just change what your +build means. + +- **`strict: true` is the default.** If you were implicitly relying on `strict` being off, + you now have every error `strict` catches, all at once. Set `"strict": false` explicitly + if you're not ready — but note you're now opting *out*, in writing, which is the point. +- **`module: esnext` is the default.** +- **`noUncheckedSideEffectImports: true`** — a bare `import "./thing"` that resolves to + nothing is now an error. This catches real bugs and a few false ones (side-effect imports + of CSS or assets need the right ambient declarations). +- **`stableTypeOrdering: true`, and it cannot be disabled.** The checker is parallel now, so + deterministic type ordering has to be enforced rather than assumed. +- **`rootDir` now defaults to `./`.** If your `tsconfig.json` lives above your sources, your + output layout just changed. Say what you mean: + + ```json + { + "compilerOptions": { + "rootDir": "./src" + } + } + ``` + +- **`types` defaults to `[]`** instead of auto-discovering everything in + `node_modules/@types`. This is a genuinely good change (auto-discovery was a common source + of "why is `describe` a global in my production build") and a genuinely annoying one, + because you must now list what you use: + + ```json + { + "compilerOptions": { + "types": ["node", "vitest/globals"] + } + } + ``` + + Missing globals after upgrading — `process`, `describe`, `expect` — is almost always this. + +## 3. Checked JavaScript got stricter + +If you run `checkJs` over a JS codebase with JSDoc types, several patterns are gone: values +can no longer stand in for types (use `typeof x`), `@enum` is no longer recognized (use +`@typedef`), the postfix `!` non-null operator is out, and Closure-style function syntax +isn't supported. Small surface, but if you're a big JSDoc shop it's the whole migration. + +## 4. The one that actually decides your timeline: no API + +TypeScript 7.0 ships with **no programmatic API**. Anything that imports `typescript` and +walks the AST or asks the checker questions does not work on 7 yet. That includes: + +- typescript-eslint (so: most people's lint setup) +- Vue, Svelte, Astro, **MDX** — anything that embeds or templates TypeScript +- Angular's template type-checking +- ts-morph, most codemods, most custom transformers + +The supported path is to run both compilers side by side: TypeScript 7 for the fast CLI +builds, TypeScript 6 via `@typescript/typescript6` (an npm alias) for the tools that still +need the API. A real API is expected in 7.1. + +So the practical question is not "does my code compile under 7" but "which of my tools reach +into the compiler". Most teams will find the answer is *at least one*. + +## What this means for a site like this one + +This site is Next.js App Router plus MDX (that's how these threads are written). MDX needs +the TypeScript API to type-check inside `.mdx`, so per the release notes, MDX projects stay +on TypeScript 6 for now. The realistic setup is split: + +- **CLI type-check and CI:** TypeScript 7. `npm run typecheck` is the single slowest thing + in our pipeline, and it's the thing every push waits on. +- **Editor and lint:** TypeScript 6 until 7.1 lands the API, because typescript-eslint and + the MDX toolchain need it. + +That's not a clean win, and it's worth being clear-eyed about it rather than performing an +upgrade for the changelog entry. A codebase this size type-checks in a couple of seconds +either way. The 8-12x is transformative for a 130-second build and a rounding error for a +2-second one, so we'll take the CI half now and move the editor when the API is real. + +## A migration order that works + +1. Upgrade to the **latest TypeScript 6** first, and fix every deprecation warning. This is + the actual work, and you can do it incrementally while your tooling still functions. +2. Modernize `tsconfig.json`: `moduleResolution` to `bundler` or `nodenext`, drop `baseUrl`, + drop `esModuleInterop: false`, make `rootDir` and `types` explicit. +3. Turn `strict` on, or explicitly turn it off, so nothing changes silently under you. +4. Inventory what touches the compiler API: lint, test transforms, codemods, framework + plugins. That list is your gate. +5. Run TypeScript 7 for CLI builds and CI, keep TypeScript 6 wherever the API is required. +6. Revisit when 7.1 ships an API and the ecosystem catches up. + +Steps 1-3 are worth doing even if you never adopt 7. That's usually the sign of a +well-designed breaking release: the migration path improves your codebase whether or not you +arrive at the destination. diff --git a/src/content/threads/why-typescript-7-is-written-in-go.mdx b/src/content/threads/why-typescript-7-is-written-in-go.mdx new file mode 100644 index 0000000..b823ee1 --- /dev/null +++ b/src/content/threads/why-typescript-7-is-written-in-go.mdx @@ -0,0 +1,156 @@ +--- +title: "Why TypeScript 7 is written in Go" +description: "TypeScript 7 shipped on July 8, 2026 with an 8-12x faster compiler. The speedup isn't a clever optimization, it's the consequence of leaving a runtime that couldn't express the thing the checker wanted to do all along." +date: 2026-07-12 +tags: ["typescript", "compilers", "go", "concurrency"] +--- + +TypeScript 7.0 [shipped on July 8, 2026](https://devblogs.microsoft.com/typescript/announcing-typescript-7-0/). +The headline is the number: vscode's build went from 125.7s to 10.6s, sentry from 139.8s +to 15.7s, playwright from 12.8s to 1.47s. Roughly 8-12x, plus 6-26% less memory. Editors +got the bigger win in practice — opening a file with errors in VS Code fell from 17.5 +seconds to 1.3. + +The interesting part isn't the number. It's that the number came from a *port*, not a +redesign. Same architecture, same algorithms, same type system, largely the same code +structure, moved from TypeScript to Go. When a straight port makes something an order of +magnitude faster, the old implementation wasn't slow because it was badly written. It was +slow because the runtime under it couldn't express what the program actually wanted to do. + +## Where the time was going + +`tsc` was a TypeScript program compiled to JavaScript, running on Node. That gives you +three costs that no amount of micro-optimization removes. + +**You pay for the JIT, every time.** A compiler run is short and cold. V8 has to parse, +interpret, profile and eventually optimize hot functions — but batch `tsc` often exits +before the JIT has finished paying itself back. The checker is a mess of megamorphic call +sites (a `Type` can be one of dozens of shapes), which is exactly the pattern inline caches +handle worst. + +**Every node is a heap object.** ASTs, symbols, types: millions of small objects, each a +V8 object with a hidden class and a pointer chase to reach any field. Type checking is +pointer-chasing all the way down, so cache behavior *is* the performance story, and +JavaScript gives you no control over layout. + +**And the hard one: you cannot share memory.** Node's concurrency story is worker threads +with structured-cloned messages or `SharedArrayBuffer` (raw bytes, no objects). The type +checker's core structure is a giant cyclic graph — symbol tables pointing at declarations, +declarations at AST nodes, AST nodes back at symbols, types recursing into themselves. To +check two files in parallel, both workers need to *read that same graph*. Serializing it +per worker costs more than the checking does. So `tsc` stayed single-threaded, on machines +where 8 or 16 idle cores were watching it work. + +That last constraint is the whole story. The checker is embarrassingly parallel by nature — +files are mostly independent, and the shared graph is read-mostly once it's built. The +algorithm wanted threads. The runtime had none to give. + +## Why Go, and not Rust + +This is where most of the noise was, and the reasoning is more interesting than the tribal +answer. + +Go's memory model is the one the compiler already assumed: a garbage-collected heap where +any goroutine can hold a pointer to any object, and goroutines share one address space. You +can spawn workers that all read the same symbol tables with no copying, no `Arc`, no +serialization. That is precisely the capability JavaScript couldn't offer, and it's where +the 10x lives. + +Rust would have fought the data model. The compiler's graph is *deliberately* cyclic: a +node points at its parent and its children; a symbol points at the declarations that create +it, which point back at the symbol; recursive types loop by construction. That's ownership's +worst case. You'd end up with `Rc>` or arena indices everywhere — either +paying the cost you were trying to avoid, or rewriting the type checker's data flow from +scratch. And a rewrite is a different, much riskier project than a port: you have to +re-derive twenty years of accumulated behavior that only exists as code. + +The port mattered for a second reason. The existing compiler is written in a function-heavy, +closure-and-data style rather than a deeply object-oriented one, which maps onto Go almost +one-to-one. That kept the port mostly mechanical, and mechanical means the type system's +thousand undocumented edge cases survive the move. + +Go isn't "faster than Rust" here. It's the language whose defaults match the shape of this +specific program, at a level of effort that made the project finishable. + +## What native + threads actually buys + +Three separate wins, worth keeping apart: + +1. **Native code.** No JIT warmup, no deopt cliffs. Structs are laid out flat, so walking + an AST touches fewer cache lines. Call this a solid constant factor. +2. **Shared-memory parallelism.** Parsing, checking and emitting run concurrently across + goroutines over one graph. This is the multiplier, and it's the one that was previously + impossible rather than merely slow. +3. **Control over allocation.** A compiler allocates in bulk and frees in bulk. In Go you + can shape that; in JavaScript you get whatever V8 decides. + +The parallelism is exposed, not hidden. TypeScript 7 adds `--checkers` (default 4) for +type-checking workers, `--builders` for parallelizing project references, and +`--singleThreaded` when you need deterministic behavior for debugging: + +```bash +# More checkers: faster on big projects, more memory. +tsc --checkers 8 + +# Reproducible ordering while chasing a compiler bug. +tsc --singleThreaded +``` + +The team reports up to 16.7x with `--checkers 8` on large projects, trading memory for it. +That knob existing at all is the tell: concurrency is now a first-class part of the +compiler's design, not an afterthought bolted onto a single-threaded core. + +There's a subtle consequence of going parallel, and TypeScript 7 handles it rather than +ignoring it. If workers check files in nondeterministic order, the order that types get +*created* in can vary, and anything that prints types — declaration emit, error messages, +union ordering — could shift between runs. So `stableTypeOrdering` is on and cannot be +turned off. Determinism is now a property the compiler has to maintain deliberately, because +the machine underneath it no longer provides it for free. + +## The editor was always the real workload + +Batch builds are the number people quote, but the language service is where developers +actually feel a compiler. It's a long-lived process holding the same graph, answering +latency-sensitive questions: hover, completions, go-to-definition, find-all-references. + +TypeScript 7 rebuilt this on the Language Server Protocol with multithreading, and the +numbers that matter aren't speed at all: an 80% reduction in failing language server +commands and 60% fewer server crashes. A single-threaded server that blocks for 17 seconds +on a big file isn't slow, it's *broken* — it drops requests, times out, and gets killed. A +lot of what everyone experienced as TypeScript being flaky in large repos was a +concurrency-starved process failing under load. + +They also rewrote the file watcher, porting Parcel's C++ watcher to Go. Watching is one of +those problems that looks trivial and is not: it's where "my editor didn't notice the file +changed" bugs are born. + +## What it cost + +TypeScript 7.0 ships **without a programmatic API**. Not a smaller one — none. Everything +that imports `typescript` and pokes at the AST (typescript-eslint, ts-morph, Vue, Svelte, +Astro, MDX, Angular's template checker, most codemods) cannot run on TypeScript 7 today. The +compatibility answer is to keep TypeScript 6 side by side via `@typescript/typescript6`, +with an API promised in 7.1. + +That's a big bill, and it's honest about the tradeoff being made. A JavaScript API handing +out object references into a graph is a very different thing to design when that graph is +being read by a pool of goroutines and owned by a Go garbage collector. They shipped the +compiler and deferred the boundary rather than shipping a bad boundary and living with it +for a decade. Worth respecting, and worth knowing before you plan an upgrade — we wrote up +[what the migration actually involves](/threads/upgrading-to-typescript-7) separately. + +## The part worth stealing + +Nobody found a magic algorithm. They took a program whose *natural* structure was a +shared, read-mostly graph traversed in parallel, and moved it to a runtime that can +represent that structure. The 10x wasn't created; it was already there, sitting behind an +abstraction that couldn't reach it. + +Which is the useful question to carry back to your own systems: not "how do I make this +loop faster", but "what is this program obviously trying to do, and what is stopping it?" +Sometimes the answer is a better data structure. Sometimes the honest answer is that the +platform you chose cannot express your problem, and every optimization from here is +interest payments on that decision. + +The TypeScript team spent a year answering that question with a full port. The rest of us +usually get to answer it for the price of a weekend spike. diff --git a/src/lib/thread-covers.ts b/src/lib/thread-covers.ts index 6695532..ed88463 100644 --- a/src/lib/thread-covers.ts +++ b/src/lib/thread-covers.ts @@ -3,6 +3,8 @@ import notesWindow from "@/images/notes-window.jpg"; import wireframes from "@/images/wireframes.jpg"; import tornPaper from "@/images/torn-paper.jpg"; import forceNote from "@/images/the-force-note.jpg"; +import colorStacks from "@/images/color-stacks.jpg"; +import planningNotes from "@/images/planning-notes.jpg"; /** * Maps a thread slug to its cover photo. Threads without an entry simply render @@ -14,4 +16,6 @@ export const threadCovers: Record = { "how-semantic-search-works": wireframes, "an-mcp-server-for-your-notes": tornPaper, "what-local-first-buys-you": forceNote, + "why-typescript-7-is-written-in-go": colorStacks, + "upgrading-to-typescript-7": planningNotes, };