Status update of my tsc port


I'm the creator of swc, a web build tool written in Rust. And I'm working at Vercel to make web development as fast as it can be. My go-to language is Rust, but I selected Go for the new TypeScript type checker.

Porting TypeScript compiler is hard

Every web developer wants a faster TypeScript type checker. But there's only one implementation of it because implementing it is way too hard. TypeScript does not have specification and tsc is the only thing which can be called a reference.

But well... tsc is a moving target, and the velocity is far from common sense. Definitely, there's no other word which can represent it correctly.

Let's compare v4.7.2 and v4.8.2. These versions are the first official stable releases of 4.7 and 4.8 respectively.

There are 311 commits and 569635 lines of codes are changed. Yes, that’s correct. 311 commits. In one minor version bump. Following tsc with swiftness, and with custom reimplementation is nearly impossible.

That's why I decided to port tsc to Go, not Rust. As TypeScript and Go shares lots of common properties, I can rewrite almost all TypeScript code in Go line-by-line. But while trying to do it, I found a better methodology.

My methodology

Porting line-by-line is easy. But... there are so many lines.

TypeScript on  release-4.9 is 📦 v4.9.1-beta via  v16.17.0 took 2s
❯ tokei src
 Language            Files        Lines         Code     Comments       Blanks
 JSON                   32         9076         9032            0           44
 Markdown                2           59            0           37           22
 TypeScript            538       346496       270968        42109        33419
 Total                 572       355631       280000        42146        33485

The src directory of TypeScript contains 346496 lines of TypeScript. Line-by-line porting is easy, but 346496 * (line-by-line porting) is definitely not easy nor funny.

No one, at least on this planet, wants to port hundreds of thousands of lines of code on one's own. As I'm one of the people living on this planet, I looked for another way. It should be much less cumbersome way than line-by-line port.

Then I got an idea.

If we want a line-by-line port, computers can help. All about computers are automation. Why can't we apply it here?

We can write a compiler which takes TypeScript compiler as an input, and emits a new TypeScript compiler as an output. Of course, output should be written in Go.

Yeah, this is insane. This is way more insane than velocity of tsc. But one thing we should note is that following tsc becomes viable if we transpile tsc to Go.

I'm an experienced compiler developer. I have been maintaining swc for several years. So I didn't need to study about compiler internals, and I just started writing a compiler.

ts2go: The transpiler

TypeScript contains a file named checker.ts, which is super big.

Guessing from the name of the file, I expected this to contain core validation logic. I chose TypeScript for the language of TypeScript transpiler, because I can access type information while transpiling.

Of course, emitting a Go file which can be consumed by go build was a hard task. But it was something doable. I simply invested lots of time. After fixing all syntax errors, checker.gen.go had 100K+ compile errors.

tsfix: Go autofix tool

I found that what I can do from TypeScript compiler is quite limited, because I can't access any information from custom Go code. So I investigated. While searching, I found that golang/x/tools/analysis supports --fix, and it supports using the type information.. Lint tool with autofix can be used to modify code automatically. So it was almost a perfect fit. Once I implemented it, it worked! I had to deal with some bugs, but it was fine.

It perform various operations. It modifies type definitions, automatically convert types, and perform much more, based on the CLI option.


While working on it, I found that it would be much better if I generate more files. So I did so. Also, I improved ts2go and tsfix over time, and I reduced the number of compile errors.

Per file:
  32 ts/internal/checker/binder.gen.go
 713 ts/internal/checker/checker.gen.go
   2 ts/internal/checker/checker_base.go
  17 ts/internal/checker/core.go
   1 ts/internal/checker/factory_node_factory.go
  31 ts/internal/checker/factory_utilities.gen.go
 117 ts/internal/checker/moduleNameResolver.gen.go
  79 ts/internal/checker/parser.gen.go
   6 ts/internal/checker/path.gen.go
  86 ts/internal/checker/program.gen.go
  23 ts/internal/checker/scanner.gen.go
 181 ts/internal/checker/utilities.gen.go
   9 ts/internal/checker/utilitiesPublic.gen.go
   1 ts/internal/checker/visitor_public.go

From 100K+ compile errors, to 1298 compile errors. Still, there are lots of tasks to do. But once the number become small enough, I can switch to manual implementation, which is quite simple and fast. It requires applying same manual patch for each update but it will allow faster release and I'll improve automatic conversion rate over time.

Moving forward

For now, my focus continues to be swc, in particular making its minifier production-ready and ensuring a successful integration into Next.js And because this is still my private project, so I can't use working hours for this. Thus, I can't guarantee when this will be ready.

© DongYoon Kang.RSS