I had a micro-heart attack this week.
I was in the middle of working through some deep implementation issues on tgo when I stumbled across a project called Goja. My stomach instantly dropped. I got this sudden, heavy sense of dread that someone had already built exactly what I’ve been pouring my life into for the last several weeks.
If you've ever tried to build something unique, you know that exact feeling. It’s the fear that you’re late to the party, that your work is redundant, and that you've been wasting your most precious asset: your time.
I stopped what I was doing and spent a chunk of time getting Goja set up so I could run benchmarks. I wanted to compare their results against my results.
The verdict? We are faster. But more importantly, Goja is not what I am building.
The Architecture of Speed: Interpreter vs. Compiler
Goja is designed for a very specific purpose: if you are writing a Go application and you want to run dynamic JavaScript scripting or plugins inside it at runtime, Goja gives you that engine. I've run into this exact engineering problem years ago with C# and Python. Goja serves its purpose well, but it is an interpreter. It runs scripts at runtime.
tgo is not an interpreter. It is a true native compiler.
Let’s be crystal clear about the difference:
- The Node/Goja model: At runtime, the engine takes your script, interprets it, and executes it on the fly.
- The
tgomodel: It takes your TypeScript code, transpiles it directly into clean Go code, and compiles it down.
The output of tgo is typed native code compiled specifically for your system architecture. When you run it, you are running a raw, optimized system binary. That is why it runs ridiculously fast. It is a complete replacement for the Node engine.
There is No Shortcut Around "Hard"
Once the panic subsided, I was left with a familiar realization: building hard things is just hard.
"Sometimes you can unlock a brilliant abstraction that suddenly makes everything scale. But other times? There is no clever shortcut. It’s just relentlessly hard work."
I find myself constantly edging toward wanting to pivot to something slightly easier, or worrying that someone has already solved this. But the reality is that no one has done this. And they haven't done it because the deep-level implementation is a nightmare.
My goal with tgo is to speed up TypeScript execution dramatically. But to do that for real-world projects, the compiler has to support actual, real-world JavaScript libraries. And this is where the real war is being fought.
The Hidden War with Legacy JavaScript
Here is the irony of building a TypeScript-to-Go compiler: I am not getting stuck on TypeScript. I am getting stuck on JavaScript.
TypeScript is structurally sound. The problem is that almost every major library has a thin layer of TypeScript types sitting on top of a mountain of messy, legacy JavaScript deep in the dependency tree.
I’m currently playing an endless game of whack-a-mole with Lodash [2]. I’m digging deep into dependencies of dependencies, dealing with raw JavaScript that does highly dynamic, obnoxious things.
In JavaScript, a variable's type can be interpreted one way at the start, and then used in a completely different, un-typed way further down the chain. Mapping that dynamic, chaotic behavior into Go's strict, type-safe compiler is incredibly difficult.
I can only imagine what's going to happen when I try to compile a library like Express, which is essentially a massive web of deeply nested dependencies. I’m going to hit an entirely new level of system-breaking problems.
We Keep Plugging
I don't know exactly how I'm going to resolve some of these nested dynamic typing issues yet. The whack-a-mole is going to continue for a while.
But as I've written about before, you have to let go of the need for immediate, easy validation [3]. You have to establish a process, keep your head down, and trust the execution [3]. When you hit a wall of extreme complexity, that is where the pretenders turn around.
The complexity is the moat. I’m going to keep plugging through.
Top comments (1)
"I am not getting stuck on TypeScript, I am getting stuck on JavaScript" is the truest line in this whole log. The typed surface is the easy 20%, and the messy dynamic stuff underneath is where the years go. Lodash is whack-a-mole, but I'd guess the wall you really hit is the stuff that has no static shape at all, like Proxy traps, prototype mutation at runtime, or a value that's a string on Monday and an object on Tuesday. When you reach those, is the plan to refuse to compile them, or to fall back to a tiny embedded interpreter for the genuinely dynamic paths and keep the native compile for everything else? Curious where you'll draw that line, because that boundary feels like the whole game.