A closer look at the details behind the Go port of the TypeScript compiler

[2025-03-11] dev, typescript
(Ad, please don’t block)

Today’s announcement by Microsoft:

[...] we’ve begun work on a native port of the TypeScript compiler and tools. The native implementation will drastically improve editor startup, reduce most build times by 10×, and substantially reduce memory usage.

This blog post looks at some of the details behind the news.

Code bases: JavaScript vs. native  

To avoid confusion, I’ll use these terms:

  • JavaScript code base: the current code base of TypeScript – which is actually written in TypeScript.
  • Native code base: the new code base. I’m using the term “native” because that’s what the TypeScript team does. The code is written in Go.

What’s the big deal?  

Type checking is the one thing that external tools can’t do:

  • Emitting .js has already become faster – thanks to native tools and type stripping.
  • Emitting .d.ts has already become faster – thanks to native tools and isolated declarations.

Thus, type checking also becoming faster is great news.

The timeline  

  • The current version of TypeScript is 5.8.

  • TypeScript 6 (JavaScript): The JavaScript code base will continue into the 6.x series, with 6.0 introducing some breaking changes to align with the native code base.

    • Original code name for TypeScript: Strada
  • TypeScript 7 (native): Once the native code base has reached sufficient parity with the JavaScript code base, it’ll be released as TypeScript 7.0.

    • Code name for the native code base: Corsa.

Both code bases will co-exist for a long time.

What needs to be migrated?  

It’s useful to note which parts of the TypeScript ecosystem are involved in the migration:

  • The command line TypeScript compiler
  • The TypeScript language server (which helps editors support TypeScript)
    • The JavaScript code base predates (and inspired) the widely-used Language Server Protocol and therefore doesn’t use it. The new language server will use that protocol, which should make it easier for editors to support TypeScript.
  • Tools that use the TypeScript code base
    • Interfacing with the Go code bases requires a completely new approach:
      • Fewer of the internals will be exposed.
      • Interactions now happen across processes.

When will the native version be ready for public consumption?  

  • Status quo: tsc works. Still missing:
    • JSX
    • Types via JSDoc
    • Build mode (project references)
  • Mid-2025: tsc with JSX and JSDoc (without build mode)
  • End of 2025: full tsc and language server

Source: “A 10x Faster TypeScript”

When did the project start?  

  • First prototype by Anders Hejlsberg:
    • Started in August 2024
    • Scanner and parser took 1–1.5 months to write.
  • Initial approach: write code manually.
  • Later: tool that automatically ports TypeScript code to Go code.
    • Porting the code worked well (with some human intervention).
    • Porting the data structures could only be done by humans – because JavaScript’s objects (with flexible types) and Go’s structs (with highly configurable data layouts) are very different. Plus, they now have to work in a concurrent setting – e.g.: The JavaScript code base orders types by giving each one a serial number when it is created. That approach doesn’t work with the Go code base where the order in which types are created is not deterministic anymore because multiple threads are involved.

Why Go and not some other programming language?  

The TypeScript team wanted to (mostly) port the JavaScript code base, not rewrite it in a different language – for two reasons:

  • The new code base must (mostly) be a plug-and-play replacement for the old code base. And that is difficult to do with a rewrite.
  • Rewriting is much more time-consuming.

If we look at the requirements for the programming language to be used for the new code base, then some of them stemmed from the decision to do a port:

  • Support for cyclic data structures – which the TypeScript code base uses a lot.
  • Garbage collection. That’s what the code base assumes.
  • The style of the JavaScript code base is more functional than OOP and doesn’t use classes often. That style is similar to how Go is written.

The remaining requirements were motivated by performance and ease of use (developer experience):

  • Good native code support on all major platforms.
  • The language should be simple and easy to approach.
  • The language should have good tooling.
  • Control over how data structures are laid out in memory. With Go, you can use structs and create an array of structs with a single allocation (vs. multiple allocations in JavaScript).
  • Good support for shared memory concurrency – which is an important element of making the code faster (more on that next).

Why not C#?  

When asked “Why not C#?”, Anders Hejlsberg mentioned the following points:

  • Go is lower-level than C#.
  • Go has better support for producing native code (including specifying the layout of data structures).
  • Go is a better fit for the non-OOP coding style used in the JavaScript code base.

Where does the 10× speedup come from?  

  • Half of the speedup comes from shared memory concurrency and using multiple cores.
  • The other half comes from native code: JavaScript has to compile just in time; it has to support a lot of flexibility for its objects; it can’t inline objects (one allocation per Array element vs. one allocation for a whole Array); etc.

JavaScript does have concurrency via web workers, but memory can only be shared in a very limited manner (see SharedArrayBuffer).

TypeScript compilation has the following phases:

  1. Parsing: producing an abstract syntax tree (AST)
  2. Binding: creating “symbol tables” for the declarations, setting up the control flow graph, etc.
  3. Type checking
  4. Emitting: code generation

In the native code base, parsing and binding can be done independently (no memory sharing necessary). Then the data structures are immutable and can be easily shared between threads.

The speed of parsing, binding and emitting scales linearly with the number of cores that are used. Taken together, they make up about a third of the total compilation time.

Type checking makes up the remaining two thirds and is not as parallelizable. Therefore, the following trick is used:

  • Type checking works for single files – it lazily pulls in more information as needed.
  • Technique: Run multiple type checkers, give each one part of the files.
  • There is not much need for thread safety: The checkers only share the immutable ASTs. Some work is duplicated, but not much because a lot of the type information is local.
  • On the other hand, the threads cannot operate completely independently – e.g. error messages shouldn’t be duplicated and be shown in a deterministic order.
  • With 4 checkers (current hard-coded number), 20% more memory is used (due to duplicated work) but checking is 2–3 times faster.
    • Note that the 20% are relative to single-checker Go – which uses half the memory of the JavaScript code base.
    • Using 8 checkers only brought an additional 20% speed improvement in tests (source).

Will the native code base run on WebAssembly?  

Supporting WebAssembly is important because it enables use cases such as online TypeScript playgrounds. And it is indeed supported.

Kevin Deng has written a “TypeScript Go Playground” that uses the new code base, compiled to WebAssembly.

Work on improving the size and performance of Go’s WebAssembly output is ongoing – i.e.: they can and will be better. Related discussion on GitHub: “Go Wasm performance”

Conclusion: an impressive achievement  

I’m impressed by how quickly the TypeScript team was able to port the JavaScript code base to Go. In one of the podcasts (see “Sources” below), Hejlsberg said “I started prototyping in August”. And I didn’t think he meant 2024 but 2023 or even earlier.

This speed is a testament to how clever the team’s approach is: If they had rewritten the JavaScript code base instead of porting it, it would probably have taken years and have resulted in many inconsistencies between the code bases.

One interesting worry expressed by Anders Hejlsberg is that, with faster type checking, people may stop trying to write types that can be computed quickly – e.g., with template literal types it’s easy to create types that require a lot of computational power.

Maybe we’ll eventually get tools for analyzing the performance of types. Type-level debugging also seems useful.

Sources