tgo Dev Log #2: Compiling UUIDs, Async Goroutines, and the Lodash Nightmare

I am about two and a half weeks into building tgo (my TypeScript to Go compiler). Over the weekend, I hit a massive milestone: I officially have the UUID library (both v12 and v13) compiling, running, and passing all of its included unit tests natively in Go.

That is a big fucking deal.

On top of that, I built out the package importing system. You can now do an NPM install, configure the tgo compiler to point to the source, and bada bing, bada boom—it pulls it in. I also added precompilation optimizations. If you aren’t changing the library, it precompiles those dependencies into Go and only updates your specific code changes when you run the compiler. It speeds the whole dev loop up significantly.

Getting the UUID unit tests to run completely was awesome. But it’s also a testament to the emotional swings of building something this complex. You get an idea, you implement it, you get positive feedback, and you’re like, Hell yeah. But then the reality seeps in of how ridiculously hard the next level is.

The AI Wall and the Need for Rigid TDD

Right now, this repository is officially one of the largest I have worked with in a long time. There are so many things to handle—from bridging the Node standard library to Go, to creating TypeScript-level linkers.

Because the project is so massive, it is starting to challenge the AI tools I’m using. I had to turn Cursor onto the 1-million-token context window. By the time the AI got enough context to understand what was happening, it had to summarize and obliterate its own thinking just to tackle the problem. Worse, AI wants to just hop in and change code to make a specific scenario work. When it does that in a codebase this deeply interconnected, it wrecks all the previous scenarios.

This forced me to get religious about TDD (Test-Driven Development). My scenario runner inputs an AST (Abstract Syntax Tree) and spits out string code. I have to force the AI to respect these scenarios. I have noticed if I don’t, the regressions are taking MUCH longer to fix. (To get the uuid unit tests passing, and then the broken scenarios, was easily 4 hours straight of token maxxing).

The Lodash Nightmare

I needed an entry point library, so I got argparse working. But then I looked at lodash. My god, compiling Lodash has been a fucking nightmare.

I have used Lodash on the backend forever. But when you are writing a compiler, you have to look at the internals to figure out why things aren’t running. Inside Lodash, there are so many obscure, low-level, weird JavaScript tricks going on. It is heavily using state, constructors, and globals. It physically hurts the head to look at.

This is exactly why I want tgo to exist. I want developers to write expressive, high-level functional TypeScript—like maps and reduces—and have the compiler optimize that into ridiculously speedy Go for loops behind the scenes. You shouldn’t have to write annoyingly obscure, low-level JavaScript just to get performance.

Async/Await to Native Goroutines

Amidst the frustration with Lodash, we had a massive win: Promises.

We implemented Promises so that they map directly to Goroutines. When you write async and await in TypeScript now, tgo runs those as independent Go routines. Depending on what your application is doing, you can get massive, huge increases in parallel performance directly out of the box. I will save my full enthusiasm, for running a web server and comparing the numbers, to see if the parallel processing hurts or helps.

The Hard Numbers: Performance Testing

I’ve been building out performance tests, and I want to be totally transparent: Go is not always faster out of the gate.

I wrote a test to compute a million primes. When you run this using standard JavaScript numbers (which are floats), Node absolutely kicks the crap out of Go. Node’s engine is incredibly optimized for float modulation apparently.

However, if you explicitly type your TypeScript numbers as BigInt (which forces direct integer math) and you run it in parallel? Go absolutely smokes Node.

Here are the raw results from the latest scenario runner:

ScenarioTotal Timetsxnodegonode → go
perf-compute-primes-million (Float, Serial)10.87s1.83s1.74s6.74s0.3x
perf-compute-primes-million-bigint (Int, Serial)14.02s5.18s5.71s2.70s2.1x
perf-compute-primes-million-bigint-parallel (Int)12.01s5.26s5.56s703ms7.9x
perf-compute-primes-million-parallel (Float)5.81s1.87s1.71s1.83s0.9x
perf-compute-primes-thousand622ms150ms30ms26ms1.2x

(Note: 5 passing in 43.34s total. Average across all tests: 1.2x. node 14.74s vs go 12.00s)

It is going to be difficult for the compiler to automatically infer an integer over a float without explicitly being told via BigInt, so we will leave these tests in as a benchmark for where we can improve. I probably spent at least 4-6 hours alone, dealing with the “is it an int or a float” and it, is surprisingly very difficult to figure out, and then even more difficult to cohere the math because Go absolutely does not like integers playing with floats directly.

Next up: diving deeper into complex type systems like my Functional Models library. I’m going to keep plugging away.

#tgo #TypeScript #Golang #CompilerDev #SoftwareEngineering #NodeJS #Goroutines #TDD #WebDevelopment

Leave a Reply

Your email address will not be published. Required fields are marked *

Copyright © 2025 – Mike Cornwell