By Tess |
December 23, 2025
I spent most of 2024 and a bit of 2025 working on tttt, a cleanroom TypeScript type checker written in C. my (admittedly ambitious) goal was type checking identical to tsc and 10X faster. I stopped development this spring for a verity of reasons: progress was slower than I would have liked, I wanted to work on other things, a bet that was part of the original motivation expired and typescript-go was announced.
on TypeScript 5.4.2's "baselines" test suite it gets identical error messages for 26% of test cases, identical symbol resolution for 41% and identical types for every expression for 24%. I never got to benchmarking and optimization as a focus, but it's 10X faster or more for most things I did test.
strictly speaking the project was a failure, however I knew this was very likely going in. tsc is built by a (presumably) competent team at Microsoft over the course of many years. building something competitive with it in one year as a solo dev was always going to be a long shot. however, as a stepping stone in my effort to Get Better At Software I would say it was somewhat successful.
development was consistent, fun and fast (even if not fast enough). I'm confident the architecture could scale to a complete TypeScript implementation with only reasonable refactors along the way. I worked on the project for so long because it was a joy to work on, and I was making real progress.
at the beginning of the project I was cautiously optimistic about C. coming from a C++ background with experience in Rust, I'd started using C for new projects (including this blog platform). I can now say with confidence the modern style of C I use continues to delight as it scales up. I would not consider writing this type of project in anything else.
I need to write a series on the specific patterns I use, but it boils down to this: writing C like its 1989 is a bad time. if you steal the good ideas from modern languages, use libraries that do the same and leverage modern tools, C code can be reasonably correct, productive and surprisingly ergonomic. C tends to add a bit of friction to abstractions compared to modern languages, but I'm unconvinced this is a bad thing. I blame excessive abstraction for problems in many of my non-C code bases, and tttt never suffered from this.
C makes up for being annoying in places by being ridiculously friendly. once you learn the basic concepts and a couple footguns to avoid, C does exactly what you tell it to without bitching or "correcting". it may be that I personally think about code in a way that's compatible with C. certainly some people seems to find Rust or pure functional languages more intuitive, and that's great for them. but for me C feels like a whole layer of mental conversion can be removed, freeing up more resources for actually solving the problem at hand.
tttt doesn't lex. it does not transform the input code into a sequence of tokens before parsing, because there's no reason to do that. it simply parses a string into an AST without formal grammars, code generators or higher order functions. this is the correct way to write a parser because it's easy and it works thank you for coming to my ted talk.
when it was starting to become clear I wasn't on track to fully implement TypeScript and time soon, I scrambled for solutions. I looked into leveraging LLMs, but they weren't very helpful at the time (and I doubt even Opus 4.5 would be much better). a more effective idea ended up being to look at what I was spending most of my time doing (or what I was spending time to avoid), and build tooling to eliminate it.
I found that re-runninng various tests and mentally parsing the output was where a lot of my time was going. I could see the "correct" results from tsc of the existing tests, but had no simple way of getting results for slight modifications. this is where the idea of diffapp came from.
In about a week I threw together a simple web-based editor. as you type it runs the code through both tttt and tsc, diffs the output, and interweaves it with the code.

this tool easily paid for itself. not only could I get results instantly from both my code and upstream, I could try way more inputs. my iteration loop went from
see failing test -> think real hard about what might cause that -> change my code -> try again
to
play with different inputs, narrowing in on the specific problem -> fix the problem -> on to the next test
a workflow I didn't consider when each iteration took seconds or tens of second instead of millis.
custom tooling can seem like an extravagance, especially for small and single-dev projects. however if tooling can make you more than twice as productive, it makes sense to spend half your time on tooling. development tooling is also the sort of thing you might be able to throw LLMs at as well, since it doesn't always have to be maintainable or particularly correct to be useful.
I haven't worked on this project for a while, and I don't plan to return to it any time soon. with enough optimization it may be able to beat typescript-go on perf, but there's far less room for improvement now. getting it into a useful state would still take many months/years, so I'm moving on. in that spirit, happy new year!