· 6 min read ·

TypeScript's Native Compiler and the Architecture Decision Behind 10x Faster Builds

Source: typescript

Looking back at the May 2025 announcement of TypeScript Native Previews nearly a year on, two things still stand out. First, the number: 10x faster on most real-world projects. Second, the language choice: Go. For a team working at Microsoft, with access to engineers who have shipped production Rust, choosing Go over Rust for a performance-critical compiler rewrite is worth examining on its own terms.

The TypeScript Compiler Is a Shared Mutable State Machine

The TypeScript compiler runs in several phases. The scanner tokenizes source, the parser builds an AST, the binder resolves names and builds a symbol table, and then the checker runs type inference and verification. The checker is the expensive phase by far, and it is architecturally unusual.

The checker does not compute types eagerly top-to-bottom. It evaluates lazily: when you ask for the type of an expression, the checker computes it on demand, caches the result, follows references to other symbols that may not have been computed yet, and builds up a shared type graph across the entire program. Types reference other types; functions reference their parameter types and return types; interfaces reference their base interfaces; generic instantiations share structure with their originals. The whole thing is one large, interconnected, mutable object graph that every part of the checker reads and writes.

This architecture made TypeScript easy to evolve over time. Adding a new type construct means adding new cases to the shared checker, not redesigning a pipeline. The cost is that the program state is fundamentally shared, and it is shared without strong ownership boundaries between different parts of the system.

Why Rust Would Have Been Painful

Rust’s ownership and borrowing rules exist to make shared mutable state safe by making it explicit and controlled. That is exactly the right model for a lot of systems code. But it creates significant friction when you are porting a program that was deliberately designed around shared mutable state.

Porting the TypeScript checker to Rust with full fidelity would require one of two approaches. The first is wrapping most of the shared type objects in Arc<Mutex<>> or Arc<RwLock<>>, which adds runtime overhead, lock contention, and substantial complexity throughout the checker’s internal logic. The second is fundamentally redesigning the checker’s data flow to separate ownership clearly, which means making upfront architectural decisions about a system with over 200,000 lines of intricate type-checking logic, before having confidence that the semantics are fully correct.

The Rust path is not impossible. The oxc project has built a Rust-based TypeScript parser and transformer that benchmarks extremely fast. But oxc focuses on parsing and transformation, which have much cleaner data flow than full type checking. A semantically equivalent type checker in Rust would require the TypeScript team to commit to a deeper redesign than they could validate incrementally.

Why Go Works

Go has garbage collection and shared memory semantics that are much closer to JavaScript’s model. Multiple goroutines can read from and write to shared data structures, using mutexes and channels for synchronization where needed, without the compiler rejecting the program because ownership does not work out at compile time.

This meant the TypeScript team could port the checker more directly, preserving the structural decisions that make the checker correct, and then add parallelism where it was safe to do so. Files without circular type dependencies between them can be checked concurrently. Program-level symbol tables can be built once and then read from multiple goroutines simultaneously. Go’s runtime handles memory management with a garbage collector, which removes the need to redesign object lifetimes across the checker’s complex reference graph.

The 10x speedup comes from two sources working together: native compilation, which eliminates V8 JIT warmup and allows better static optimization, and structured concurrency across the checking phase. Neither benefit alone gets you to 10x. Combined, on a large project, they do.

There is also established precedent. Esbuild, Evan Wallace’s JavaScript and TypeScript bundler written in Go, demonstrated years earlier that Go could produce tooling 50 to 100 times faster than equivalent JavaScript implementations. The TypeScript team did not have to bet on an unproven approach; they had a concrete reference point for what Go can do in this domain.

What 10x Means in Practice

On a large TypeScript project, tsc can take tens of seconds to produce its first diagnostic. Language servers running inside editors have to stay responsive while doing ongoing incremental checking, and on complex generic code or deeply nested conditional types, they can stall visibly. These are not abstract benchmark differences. They affect whether you have a fast edit-check-fix loop throughout the day.

The native previews, distributed as a binary alongside or in place of the standard tsc, bring cold-start type checking on large projects from the tens-of-seconds range down to low single digits. The language server improvements are proportional. Editor responsiveness for completion, hover types, and go-to-definition depends on the same type checker internals, so all of those tighten up accordingly.

For CI pipelines, the implications are more significant than they might first appear. TypeScript type checking is often the slowest single step in a build, frequently slower than compilation, bundling, and many test suites. A 10x reduction there does not just save time; it changes what you are willing to do. Teams that previously ran type checking only on pull requests might run it on every commit. Projects that split into many packages with project references as a workaround for build performance might simplify back toward a more straightforward structure.

What the Native Previews Actually Ship

The native compiler maintains compatibility with tsconfig.json and the existing project system. It is not a new language or a new dialect. The goal is a drop-in replacement for tsc that produces the same diagnostics with the same correctness guarantees, faster. The May 2025 announcement described a preview that handled the majority of real-world TypeScript programs correctly, with documented gaps around more exotic uses of conditional types, template literal types, and certain decorator patterns being actively closed.

The language server component, which editors like VS Code use for interactive features, is included. This is the part most TypeScript developers interact with continuously, and it benefits from the same underlying performance improvements as the batch type checker.

The Broader Ecosystem Shift

For years, the practical advice for TypeScript build performance was to skip type checking during development builds and use tools like esbuild or swc for fast transpilation, then run tsc --noEmit separately for correctness. That split exists because tsc was slow enough that mixing it into the hot path of a development build was a meaningful tradeoff.

If the native compiler lands with stable correctness across the full TypeScript feature set, that tradeoff changes. A 10x faster tsc might be fast enough to sit directly in the development build loop for many projects, eliminating the operational complexity of maintaining two separate tool chains with different semantic coverage.

The TypeScript compiler API, which powers type-aware linters like typescript-eslint and various type-level testing tools, also benefits. The native compiler exposes the same API surface through appropriate bindings, so tools built on top of the compiler API get the speedup without requiring changes to their own code.

A Year Out

The decision to use Go reads as principled rather than opportunistic. The TypeScript checker’s architecture made Go the path of least resistance to a semantically correct port with meaningful parallelism. Rust would have been a more defensible choice from a contemporary systems-programming reputation standpoint, but it would have required either accepting lock overhead throughout the checker or committing to a deeper architectural redesign. Go preserved the existing structure and allowed parallelism to be added incrementally, which is a better fit for the scale and complexity of the problem.

The 10x number is real, and it reflects genuine architectural work rather than a straightforward native compilation win. Whether the native compiler ships as the default in a future TypeScript release or continues as a parallel option alongside the JavaScript compiler, it changes what fast TypeScript tooling means. The bar just moved.

Was this interesting?