March 11, 2025·6 min read·engineeringtypescriptcompilerperformancearchitecture

TypeScript's Go port puts compatibility ahead of a new checker design

Microsoft is porting the TypeScript compiler and tools to Go, with roughly tenfold checking improvements in its early results. Preserving existing checker behavior is the central constraint, while compiler APIs need a different integration model.

A type checker can accept the same syntax and still break a large ecosystem by choosing different inference results. Libraries encode assumptions about those results into their public types. That makes replacing TypeScript's implementation a compatibility problem whose scope extends well beyond parsing the language.

Microsoft's March 11 announcement describes a native port of the compiler and tools to Go. The project, codenamed Corsa, targets roughly tenfold faster checking and lower memory use. It is work in progress, with a command-line checking preview planned for the middle of the year. TypeScript 7 is the intended release identity once the native implementation reaches sufficient parity.

I remain cautious about TypeScript as a language choice, but the quality of its web ecosystem is difficult to dismiss. That ecosystem makes this port consequential. A faster checker is useful only if the frameworks and libraries that make TypeScript worth using continue to work under its judgments.

Porting preserves more than accepted programs

Ryan Cavanaugh's explanation of the porting strategy distinguishes translating the existing implementation from starting a new one. The team's priority is preserving behavior and structure closely enough to carry existing semantics and optimizations into the new codebase.

Type inference often permits multiple reasonable results. A programmer can still depend on the particular result produced by one implementation. Recreating an abstract idea of TypeScript would therefore be insufficient if it changed those choices across popular libraries.

The same reasoning applies to performance behavior. An optimization can become necessary for checking a library's types within a useful time. Removing it may preserve a theoretical answer while making the implementation unusable on real source.

That is an unusually demanding definition of compatibility. It includes limitations and quirks that would be attractive cleanup targets in a fresh implementation. Preserving them can be the right migration choice when users have built working programs around the current behavior.

A structurally similar port also helps the transition itself. A bug fix in one codebase can be mapped to the corresponding operation in the other. That reduces the cost of maintaining two implementations while users establish whether the new one meets their requirements.

The implication is that implementation language and language semantics are separate decisions. Choosing Go does not mean redesigning TypeScript around Go's type system. The new program still has to reproduce the behavior of the TypeScript checker.

Go fits this particular translation

The team's language-choice explanation says it evaluated multiple languages and hybrid designs. Go performed well under constraints specific to a faithful port. The authors emphasize structural similarity, control over memory layout, and garbage collection compatible with the compiler's workload.

Compiler data structures contain extensive graph relationships. Trees need traversal in different directions, with nodes whose concrete kinds vary. The port needs to preserve how the existing implementation represents and processes those relationships without introducing an unrelated redesign.

Garbage collection is part of that tradeoff. A batch compiler process can terminate after its work, while an editor service retains many of its initial syntax structures for the program's lifetime. The team argues that these lifetimes make the costs acceptable for this application.

That does not establish that Go is the best language for every compiler. It explains why a language can fit a migration whose principal requirement is preserving an existing architecture. A ground-up compiler design would be a different engineering problem.

The explanation also acknowledges weaker in-process JavaScript interoperability. That weakness matters because TypeScript is both a command-line tool and a library used by other JavaScript tools. Faster internal processing does not remove the need for a usable integration boundary.

Early timings establish an order of magnitude

The announcement includes comparisons on several public projects. It reports the VS Code check dropping from 77.8 seconds to 7.5 seconds and Playwright from 11.1 seconds to 1.1 seconds. Those are results reported by the TypeScript team, not measurements from this post.

The implementation is not feature-complete. Its timings therefore support a claim about the demonstrated performance direction, not complete drop-in compatibility for every project. The team itself presents them as evidence for the order of magnitude it expects.

That distinction is useful because checking time includes work that depends on the source and configuration. A project that exercises an unfinished feature cannot judge readiness from a timing on a different project. A fast partial result and a correct complete result are different observations.

The editor has another performance profile. Loading a project determines when semantic navigation and other services become available. Improving that path can matter even when the command-line build is not the developer's immediate bottleneck.

The release plan treats command-line checking, project builds, and the language service as milestones. Their relationship matters because a compiler invocation and a persistent interactive service have different lifetimes and obligations. The ability to load and check a project does not prove that every editor operation is ready.

A faster implementation can support more semantic work

The architectural opportunity is broader than reducing a waiting period. If semantic queries become cheaper, an editor can consider work that was previously too expensive to perform across an entire project. The announcement identifies comprehensive errors and more advanced refactoring among the intended opportunities.

Those remain opportunities, not completed features. A performance improvement creates room in an execution budget. Implementing a reliable refactoring still requires defining its behavior and preserving program meaning.

This distinction is particularly relevant to tools requesting large amounts of semantic information. A service can have a fast checker while an integration asks for data in a way that introduces substantial overhead. The implementation and the API have to be considered together.

My interest is in the amount of reliable information the editor can provide while working on an application. TypeScript's framework ecosystem rewards adherence to detailed conventions. Responsive navigation and diagnostics help make those conventions visible where the code is being changed.

The compiler API cannot simply remain an object graph

Daniel Rosenwasser's API discussion says API consumers will generally communicate through message passing across a process boundary. The team does not expect to expose every existing operation unchanged.

That follows from moving the compiler into a separate runtime. An object graph that JavaScript callers can directly inspect or modify becomes a distributed interaction if reproduced through individual remote calls. Serialization and call overhead then become part of the API's performance.

The team describes experiments with synchronous standard-I/O communication and a JavaScript client. It also considers batching operations to reduce overhead. The public discussion is exploratory, so it should not be read as a final API contract.

A curated API creates a tradeoff. Explicit operations can let the implementation change internal representation without breaking every consumer. Existing tools that reach into those representations may need a new way to express what they actually need.

That is a consequential migration boundary even if command-line behavior becomes highly compatible. A build invoking a binary and a tool embedding the compiler have different dependencies. The same source project may be ready for one integration before another.

The announced move to Language Server Protocol is another boundary decision. Standardizing editor communication can separate the implementation from the editor more clearly. It does not automatically preserve every extension mechanism built around the existing language service.

The JavaScript line remains part of the plan

The announcement says the JavaScript implementation will continue into the 6.x line, with TypeScript 6.0 introducing deprecations and changes that prepare for the native implementation. TypeScript 7 is reserved for the port after sufficient parity.

This provides a migration period for projects with legacy configurations or API dependencies. The plan explicitly allows some users to remain on the JavaScript implementation while the native one matures. It is not a declaration that every project should replace its compiler immediately.

Maintaining two related implementations is additional work, which makes the structural-port strategy relevant again. Compatibility and shared fixes are transition mechanisms as well as technical goals. The native implementation needs feedback from real projects without abandoning the existing tool those projects rely on.

The concrete result today is a public Go implementation, early performance evidence, and an explicit compatibility strategy. Application authors can follow readiness of checking and editor support. Tool authors have a separate API question to track, because preserving TypeScript's answers does not preserve unrestricted access to the old compiler's internal objects.