This article is published in English.
Picking a Language by Problem Shape: Lessons From TypeScript's Go Port
What the choice of Go for the native TypeScript compiler teaches about matching tools to workloads, valuing tooling speed and porting large codebases step by step.
Waiting close to a minute for autocomplete in a large TypeScript monorepo is a familiar kind of pain, and it is the same pain mobile developers feel while Xcode indexes a big project. The TypeScript team fought exactly this problem, and the way it chose to fix it is a useful case study in making technology decisions on evidence rather than fashion. This article looks past the benchmark headlines and pulls out the engineering reasoning you can apply to your own tools and apps, whether you ship TypeScript, Swift or both.
What Microsoft announced
In March 2025, Anders Hejlsberg, who created both TypeScript and C#, announced that Microsoft was building a native port of the TypeScript compiler and its language tooling. Many people assumed the target would be Rust, or perhaps C++. It was Go.
The native implementation, code-named Corsa, is the basis for TypeScript 7.0. At the time of writing, the timeline pointed to a release around mid-2026, so check the official TypeScript blog for the current status before planning a migration.
The reported numbers were large. Loading the VS Code codebase in the editor dropped from roughly 9.6 seconds to 1.2 seconds, memory use was cut roughly in half, and Microsoft described project loading as about 8x faster overall with type-checking up to 10x faster. If you want the details of how those gains land in real projects, we have covered them separately in what the Go rewrite changes without touching your code. The rest of this piece focuses on the decision itself.
Why Go instead of Rust
The team was looking for the lowest-level language that still produced native code on every platform TypeScript has to support, while offering good built-in concurrency. Go fit that description.
Two further factors mattered. First, the workload: type-checking a large project is largely a matter of checking many files in parallel and then combining the results, and goroutines map onto that pattern very directly. Second, the existing code: the compiler is a huge JavaScript codebase that relies on garbage collection and a fairly functional style. Go also has a garbage collector and a similar structure, so the code could be translated closely. Moving it to Rust would have forced a much steeper redesign around ownership and lifetimes, a heavy cost for a large team with an enormous codebase to carry over.
Put simply, the team did not choose the most fashionable language. It chose the one that matched its concurrency model and kept its engineers productive. That is an architectural judgment, the same kind of judgment you make when deciding between UIKit and SwiftUI, or between Combine and plain async/await for a particular feature.
The same idea in Swift
Swift developers have met this problem class before. Structured concurrency was added to Swift precisely to run expensive work in parallel without sacrificing correctness. The snippet below expresses the "check many files at once, then merge" idea with a task group: each file gets its own child task, and the parent collects the diagnostics as tasks finish. Note that the snippet is Swift, even though it illustrates the pattern the Go compiler uses with goroutines.
// Swift concurrency: parallel "type-checking" of files, similar in spirit
// to how Go's goroutines let TypeScript check many files at once
func checkFiles(_ paths: [String]) async -> [Diagnostic] {
await withTaskGroup(of: [Diagnostic].self) { group in
for path in paths {
group.addTask {
await checkSingleFile(path) // runs concurrently
}
}
var results: [Diagnostic] = []
for await diagnostics in group {
results.append(contentsOf: diagnostics)
}
return results
}
}
A few things are worth noticing. withTaskGroup guarantees that every child task finishes before the function returns, so no work leaks out of scope. Results arrive in completion order, not input order, so if the order of diagnostics matters you need to sort them afterwards. And the parallelism is only safe because each check is independent, which is exactly the property that makes type-checking a good fit for Go's model.
If you have adopted Swift 6, you have already made this kind of trade: you pay up front to satisfy strict concurrency and safety checks, and the return comes later as faster builds and more stable runtime behavior. Microsoft made a similar bet with its compiler.
Lessons that carry over to your own projects
- Tooling speed is a product feature. Slow builds and laggy autocomplete impose a daily, largely invisible tax on every developer on a team. Performance work on build pipelines, editors and indexing is as legitimate as performance work on the shipped app.
- Let the problem, not the trend, choose the tool. Whether you are picking MVVM, SwiftData or Combine, the right answer follows from how data actually flows through your system, not from what is popular on social media this month.
- Rewrites can be incremental. The TypeScript team ported the existing compiler as faithfully as possible instead of redesigning it, so the new version produces the same results as the old one. That is a strong argument against rebuilding an entire app architecture in a single sprint.
When should you not follow this example? If your current stack is not the bottleneck, a port buys you nothing but risk. Measure first, and make sure the slow part really lives in the layer you intend to replace.
Recognizing the warning signs
The symptoms that drove this project tend to appear in any codebase that grows large: type inference that slows down, editor feedback that lags, and build graphs that keep expanding. The fix is not always a new framework. Sometimes it means looking closely at what your tools are actually doing under the hood and changing the part that is doing too much work.
Key takeaways
- Go was chosen because its native compilation, garbage collection and goroutine-based concurrency matched both the type-checking workload and the structure of the existing code.
- The speedups came from a faithful port, which kept behavior consistent while changing the runtime underneath.
- Treat developer tooling latency as a real cost worth engineering effort.
- The next time a build crawls, ask the question the TypeScript team asked: which tool actually fits the shape of this problem?