I have spent the better part of two weeks grinding on getting Lodash supported in the tgo compiler. It has been a relentless game of whack-a-mole, constantly dealing with dependencies of dependencies deep down in the JavaScript ecosystem. It’s a process of taking ten steps forward, two steps back, and breaking UUID implementations just to get a Lodash function to compile.
But in the middle of this grind, I stumbled onto a massive breakthrough: I noticed that Lodash has JSDoc strings outlining the internal function types.
Not only were the JSDocs there, but the TypeScript compiler’s library has the built-in ability to parse that code. So, I added the capability for tgo to find those JSDocs and generate the typing information based directly on them. Immediately, right out of the gate, we jumped up a massive amount of support for Lodash.
It also sparked a much larger realization about how we build software, how we document it, and why dealing with legacy JavaScript is so agonizingly difficult.
The Myth of "Dead" Documentation
People love to come up with these stupid silver-bullet ideas for software engineering, and it drives me crazy. Whenever somebody does something stupid, there is always some person that says...
"We can solve this by adding documentation. That will stop the next person."
I close my eyes, put on the unamused face, and wonder when a meteor will just come by, hit the planet and kill us all.
As someone pushing 40, who has executed projects across wildly different domains—from computers, to running a farm, to business—I refuse to operate in anything other than the real world. The real world is organic, uncertain, and infinitely harder than pure logic dictates. This may come to a shocker to some but, words written down, literally don't do anything and don't "ensure" anything.
Because of that reality, I am not a fan of dead documentation.
Documentation is really hard to get right as well. (Not just it is exceptionally costly to maintain) What happens most of the time is there is some reminder as a generic checklist item in a sprint that says "update documentation." Unhelpful as hell, and never works.
Much to my surprise, even AI struggles with this too. Turns out being flippant and waiving a magic wand, doesn't actually work. "Which documentation, and how should it be written?" Even the most powerful, high-context AIs don't magically know all the five different, disconnected places a system might need its docs updated, and putting all that information in a ticket to implement, not worth someone's time.
The developers who need documentation to tell them what the code does, should get another job.
I'm not intentionally trying to sting people by saying these things, but its true. It's always the managers (people who's work is people) that think that documentation is the way that somebody should know how a computer works.
We could also just literally let the computer tell us what it does.
The only time I read comments is when a section is doing some obscure, weird shit—like a mathematical optimization that abandons readability for pure speed. But 99% of the time, that logic should just be encapsulated in a small function with a good name. A good function name is functional documentation.
I love that IDEs make comments a muted gray color on a black background. It keeps them hidden away so they don’t distract you from reading the actual code.
Note how much harder it is to maintain documentation, when people try to literally make it not see-able.
The Superiority of Functional Documentation
The only documentation I truly love is functional documentation.
Dead documentation doesn't do anything. You can't put a unit test on a README. But functional documentation—like an OpenAPI YAML file, or a JSDoc string—gives you human readability and machine functionality. It drives the system.
For mid-level engineers, this is a crucial architectural dividing line. When tgo parses JSDocs, it essentially turns raw JavaScript into basic TypeScript. But it is a double-edged sword: if you use JSDoc for tgo, your shit better be right. It gut-checks you. If the functional documentation is wrong, the machine physically breaks and the library becomes uncompilable. (Cough, that's what Typescript is)
For those who worry that their documentation will break their actual code (HA), I did add the ability to say to ignore documentation (like real computer people do).
I've complained quite a bit about documentation but don't get me started on technical diagrams. Diagrams are the worst because people (myself 100% included) try to overlay multiple, conflicting layers of data on top of a single visual (e.g., drawing an HTTP client-to-server connection arrow, and then confusingly drawing data-flow arrows in reverse). Systems are too complex to be reduced to a single generic chart. Use the right format for the right slice of data.
The Stupidity of JavaScript Hoisting vs. The Strictness of TypeScript
Dealing with legacy JavaScript has been what makes this compiler so hard to build.
One thing that has been really, really annoying right out of the gate is hoisted variables in JavaScript. I didn't even fully realize how bad this was until recently, let alone even hear the term 'hoisted'. I start trying to fix something else in lodash, run the tests... and... "well there ya go again, 'lang-var-hoisting' is broke.. YET AGAIN"
The concept of hoisting—using a variable before it is declared—is completely stupid. It breaks the most basic, fundamental concepts of logic and system design.
Here is a VERY VALID javascript example
const run = () => {
console.log(hoisted);
var hoisted = 5;
console.log(hoisted);
};
run();
I want to gouge my eyes out reading that code. WHY.
Now, hoisting a main function to the top of a file while the helper functions sit underneath it? I have no problem with that. It makes sense within the larger scope. But hoisting a variable? It’s a nightmare. I’ve had to deal with hoisting issues in the compiler a million and a half times.
I see nothing wrong with this below:
const main = () => {
dependency1()
dependency2()
}
const dependency1 = () => {}
const dependency2 = () => {}
main()
This is why, despite my frustrations, I have to appreciate TypeScript. I wouldn't necessarily call TypeScript "elegant," but when you have a properly typed TS library with minimal jankiness, it just works.
TypeScript acts as a forcing function. A lot of the wackiness and over-the-top dynamicness that happens in pure JavaScript simply doesn't happen in TypeScript. Why? Because developers realize it’s incredibly hard to type that kind of chaotic behavior. I do feel a little guilty at times when i do:
//@ts-ignore
const somethingBad : string = 1
Injecting Native Go Power into TypeScript
While grinding through legacy JavaScript feels like an endless chore, extending the compiler with native features is incredibly fun and shockingly fast.
I wanted to give tgo developers the ability to seamlessly transition into using native Go features directly inside TypeScript. Specifically, I looked into the complex128 type in Go—which natively handles real and imaginary numbers for complex mathematics.
Adding this capability to tgo was ridiculously easy compared to supporting a legacy JS library like Lodash. We just added a Node library with the typing for complex numbers, wrote a little bit of wrapper Go code, and mapped it in the compiler.
I had an AI suggest a performance benchmark, and we landed on running a million iterations of a complex Mandelbrot set. The results speak for themselves:
Scenario: perf-complex-mandelbrot-million
- Node (tsx): 313ms
- Node (raw): 205ms
-
tgo(Go Native): 79ms - Result:
tgois 2.6x faster natively, without any extra optimization. - Result: The raw language complex math test was 4.5x faster (27ms vs 6ms).
You write standard TypeScript, and the compiler turns it into "optimized" compiled binaries.
Ding!
The Looming Dependency Nightmare
The contrast is wild. Adding net-new, highly complex native capabilities takes almost no time. Even re-implementing entire Node standard library capabilities is easier than getting lodash to compile into Go.
I am still waiting to see how tgo is going to handle the deep, dark dependency trees of the NPM ecosystem. When you do a simple NPM install for a web framework like Express, you often don't realize there are 500+ hidden libraries underneath it that all need to be compiled. That is going to be incredibly complex to untangle. Early on, looking forward at some of this, its possible I might have to start using a different server library, or roll my own. (Sigh). Unfortunately things like @modelcontextprotocol/sdk use Express, so we'll see.
Top comments (0)