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.
- Related: I'm porting tsc to Go
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.
tsc is a moving target, and the velocity is far from common sense.
Definitely, there's no other word which can represent it correctly.
These versions are the first official stable releases of
There are 311 commits and 569635 lines of codes are changed.
Yes, that’s correct.
311 commits. In one minor version bump.
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.
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 ===============================================================================
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
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
--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
tsfix over time, and I reduced the number of compile errors.
1298 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.
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.