The New Blub Paradox, or: Why TypeScript Is a Poor Choice for the AI Era
Defaulting to TypeScript because the models are fluent in it echoes Paul Graham's twenty-five-year-old Blub Paradox. The AI era turns TypeScript's unsoundness from a quirk into a liability.
In a multilingual codebase, which language should be the default? The one you reach for when nobody’s made a deliberate case for the other? It used to be a straightforward calculation: hiring, ecosystem, what the team already knows. Lately a new argument has muscled its way into the mix: default to whatever the AI writes best. And for many teams, that’s apparently TypeScript.
I think this reasoning is a twenty-five-year-old mistake wearing new clothes.
And to be clear about scope: if you’re shipping anything that runs in a browser, you can’t really avoid TypeScript, and pretending otherwise is silly. There are also genuine reasons to reach for it deliberately, a team that already lives in it, a library ecosystem you actually need, a frontend that has nowhere else to go. My quarrel isn’t with using TypeScript where it’s appropriate. It’s with making it the unexamined default for everything on the strength of “the AI likes it.” On most other counts I find it rather weak, and that’s the case I want to lay out.
The old paradox
Twenty-five years ago Paul Graham wrote that your choice of language is a competitive weapon. His startup won, he argued, partly because they wrote it in Lisp while competitors slogged through C++ and Perl. The Blub paradox: a programmer fluent in a middling language can recognize everything less powerful than what they use, but is constitutionally unable to see what’s more powerful. From inside Blub, the more expressive language just looks weird and unnecessary.
We’re relearning this lesson, except the pressure isn’t peer comfort or hiring pools anymore. It’s AI. And the language many people seem to be settling on, the new Blub, the agreed-upon reasonable middle, is TypeScript.
The pitch for TypeScript in the LLM era is seductive because it sounds like a win-win rather than a capitulation. You’re not going full dynamic-language cowboy. You get types! The models are spectacular at it because they’ve digested every line on GitHub. It’s the sensible center: enough structure to feel disciplined, enough corpus to keep the autocomplete fluent. Why would you reach for something more exotic?
Here’s why. TypeScript is the worst of both worlds in exactly the dimension that now matters most. The AI era turns its central design compromise (that is, an unsound type system that eases JavaScript integration) from a quirk into a liability.
Unsound on purpose
TypeScript’s type system is unsound by design. By unsound, I mean that it permits type errors at runtime. This isn’t a bug or an unfinished corner. It’s a stated goal. The team will tell you directly that they chose to permit type errors at runtime in exchange for pragmatism and JavaScript compatibility. as casts launder lies past the checker. any is a hole straight through the floor, and it’s contagious; one any upstream readily causes whole call chains to lose the type-level guarantees they try to enforce. Array indexing hands you back a T while cheerfully returning undefined at runtime. Objects have more keys than their types admit, and may not have the stated keys depending on the provenance of the object in question (e.g. an object parsed from JSON). The compiler that’s supposed to be checking the model’s work has, by its own charter, agreed to look the other way whenever it would be inconvenient not to.
Now put that in a world where machines generate large volumes of plausible code you didn’t write and won’t fully read. What’s actually catching the model’s mistakes?
In a sound type system, a large fraction of plausible-looking errors simply don’t compile. The invariant you encoded forbids them, full stop, on every build, in seconds, without anyone getting tired. That’s the cheapest and most scalable error correction you have, and its value goes up when generation speed goes up, because your reviewers and your test suite don’t scale with the model, but the compiler does.
Fundamentally, TypeScript gives you the appearance of machine-checked safety without the guarantee. The green checkmark you get from the TypeScript compiler says the generated code typechecks; then you ship to production, and the runtime says cannot read property 'x' of undefined. In a hand-written codebase you might internalize where the soft spots are. In a generated one you’re trusting a checker that has formally reserved the right to be wrong, applied to code authored by something that is also allowed to be wrong, and you’re calling the combination “type-safe.”
Current LLMs, meanwhile, are extremely good at producing TypeScript that typechecks and is subtly wrong, because the training corpus is saturated with exactly that. It knows every idiom for satisfying the checker, including the escape hatches. Ask for something tricky and watch how readily an as unknown as T or a stray any appears to make the red squiggle go away. LLMs have learned that the goal is a passing tsc, and TypeScript has provided a rich vocabulary for passing tsc without writing correct code. You get a confident generator of code that clears a bar specifically designed to be clearable without correctness.
Beyond that, TypeScript’s view of any library it doesn’t compile itself comes from a .d.ts file: a hand-written claim about JavaScript that nothing checks against the actual code. It’s a type annotation with no implementation tethering it to reality, frequently authored by someone who isn’t the library’s author, for a version that has since changed. The compiler treats this unverified assertion as ground truth. In my experience, these declarations are routinely imprecise or outright wrong: a nullable return typed as non-null, a fistful of any papering over anything awkward, an object shape missing a field the runtime actually returns, a @types package that covers v3 but has bad version bounds, while you’ve installed v5.
Now hand that to a model. It builds its understanding of the library from those declarations, generates code that satisfies them perfectly, and the compiler blesses the result. You’ve got an unreliable narrator reading from an unverified map, refereed by a checker that’s agreed not to look too hard. Three layers of “probably fine” stacked into a single green checkmark.
Compare what a sound, expressive language does in the same loop. When you make illegal states unrepresentable and actually enforce it (no escape hatch that silently downgrades the guarantee), the model’s plausible mistakes hit a wall. The types are also context: a precise signature constrains what the function must do far more tightly than TypeScript’s structural soup, where any object of the right shape slips through and any dissolves the constraint entirely. The compiler’s feedback is a correction signal the model acts on, and in a sound system that signal actually means something. The languages people call “hard for AI” tend to be the ones that hand the AI the most leverage once it’s in the loop, because they front-load meaning that TypeScript leaves implicit, optional, or castable-away.
So the real comparison isn’t “TypeScript, the safe sensible middle” versus “some scary academic language.” It’s “a model fluent in producing code that satisfies a checker engineered to be satisfiable without correctness” versus “a model whose plausible errors a sound compiler actually rejects.” TypeScript in the fine print declines to enforce the rules it appears to be enforcing. That was a tolerable trade when a human wrote and read every line and carried the soundness gaps in their head. It is a bad trade when the human-in-the-loop is ovewhelmed by a tidal wave of code.
I’ll diverge a bit here from typechecking considerations to discuss security. TypeScript doesn’t have its own supply chain. It compiles to JavaScript, runs on some JS runtime, and installs from the same registry. Whatever you think about npm, you’ve signed up for all of it the moment you pick TypeScript. And npm is, by a wide margin, the most aggressively attacked package ecosystem in software. Sonatype’s 2026 report counted more than 454,600 new malicious open-source packages in 2025, pushing the cumulative total past 1.2 million, a 75% jump year over year. This isn’t a bad year; the people tracking it are explicit that it’s the new baseline in the dawn of the AI era.
And these aren’t all stupid CVEs either, unlikely to cause real problems. In September 2025 a single phishing attack compromised 18 widely-used npm packages seeing over 2.6 billion downloads a week, injecting code that rewrote cryptocurrency transactions in the browser. Days later came Shai-Hulud, the registry’s first genuinely self-propagating malware: it stole maintainer tokens, used them to reach more accounts, scanned for more secrets, and republished itself into further packages, looping without human intervention. Its core mechanism was the malicious post-install script: arbitrary code that executes the instant an infected package is deployed. A separate campaign that same autumn flipped over 5,500 private repositories to public and ultimately compromised 526 packages.
This is the thing the “AI is so fluent in TypeScript” argument I’ve seen so far doesn’t account for. The fluency is downstream of an enormous corpus, and that same enormousness (the gravitational pull of npm as the center of the software universe) is exactly what makes it the richest target for attackers. You don’t get the corpus without the blast radius. And a sound type system, the one real advantage I’ve been defending, does nothing for you here. Soundness verifies that your code is internally coherent. It says nothing about whether the dependency you just installed is secretly running TruffleHog against your environment variables.
So when you choose TypeScript to make the model happy, you are not choosing a language. You are choosing a registry, a runtime, and an attack surface: the one with industrialized, self-replicating malware and three-quarters annual growth in poison packages. Coupling your security to the ecosystem that gets hit most often, most automatically, and most creatively is a poor bet no matter how fancy the LLM is at writing TypeScript.
There’s a final, almost comic turn here. Security researchers assess that the recent npm worms were themselves likely written with AI assistance, citing tell-tale comments and emojis in the malicious scripts. The same generative fluency being sold as a reason to live in this ecosystem is being used to attack it. The codegen firehose points both ways.
I want to be careful not to oversell the alternative, because the easy version of this argument is a lie. Haskell et al are not safe here by virtue of being fancier languages. Hackage can host malicious packages; Cabal will happily run a compromised Setup.hs, and Template Haskell executes arbitrary IO at compile time, which is if anything a juicier hook than a post-install script. The main reason the ecosystem doesn’t have a Shai-Hulud is that no one has bothered to build one, and “no one is aiming at us” is security by obscurity. If the attention shifted, the incidents would follow.
A few differences are cultural: the community prefers fewer, larger libraries, so the transitive closure is smaller and there are fewer independent maintainer accounts inside your trust boundary; the norms are slower and more pinned, shrinking the window a poisoned version has to sweep into a build; packages largely ship source, not pre-built binaries, so there’s no opaque blob to audit. Those help. None is a guarantee, though.
There’s a second pitch that usually rides shotgun with the AI one: TypeScript has the bigger ecosystem. Every problem you’ll hit has a battle-tested package, every framework ships first-class types, the tooling is polished to a shine. Reach for npm and someone has already solved it.
Notice that this argument conflicts with the one sitting right next to it. The whole reason we’re defaulting to the AI’s favorite language is that the AI lets us write more code, faster. Conjuring up an implementation has gone from a week of work to an afternoon of prompting. But if that’s true, the marginal value of a sprawling package ecosystem is collapsing in real time. The thing the ecosystem buys you is not having to write the code yourself. The thing the model buys you is… not having to write the code yourself. It’s the same outcome either way. The more you believe the productivity story of AI, the less the ecosystem story is worth, because you can magic up a focused, dependency-free version of that date-parsing or state-machine library in the time it’d take to evaluate three candidates on npm.
And the version you generate wins relatively easily on the security axis in that it’s code you can read, doing only what you need, with no transitive closure of strangers’ packages dragged in behind it. Every dependency you don’t take is an attack surface you don’t inherit and a .d.ts you don’t have to trust. So the ecosystem advantage and the productivity advantage are partly the same advantage counted twice. And in TypeScript’s case, the half you’re double-counting is precisely the half that hauls the npm blast radius along with it.
A newer, mythier man-month
Fred Brooks gave us the canonical software-management fallacy fifty years ago: adding people to a late project makes it later. The reason wasn’t that people are useless. It was that the binding constraint on software was never raw labor. It was the cost of communication between the parts and the people, growing combinatorially, and the irreducible fact that some work is sequential. You cannot make nine women produce a baby in one month.
AI assistance looks like it repeals this. Labor is suddenly cheap and instant; the marginal contributor is free. So generate more, generate faster. But Brooks’s bottleneck was never the cost of producing code. It was the cost of making the pieces cohere (integration, reconciliation, verification), and that cost doesn’t fall when you add a tireless generator. It rises. You haven’t added a colleague you communicate with, no matter how many types Claude tells you “you’re absolutely right”; you’ve added a firehose you must communicate about to your human colleagues, after the fact, by them reading every line and checking it against everything it touches. That checking is precisely the sequential, un-parallelizable work Brooks told us we can’t escape by adding throughput.
This is the mythical man-month at a higher and more dangerous level, because the old version at least added contributors who carried context and judgment. The new one adds volume that carries none and bills the entire integration cost to the humans downstream. And there is exactly one tool that scales the integration check without adding a human link to the communication graph: a sound compiler. A type system that makes incoherent combinations non-representable absorbs a whole class of “do these parts fit” into a machine that answers in seconds. It is the only thing on the table that actually absorbs some of the network costs otherwise incurred by Brooks’s Law. So adopting the firehose and simultaneously choosing the unsound language is the fallacy in its purest modern form: you scaled the production of work and deliberately crippled the one mechanism that scales the integration of it. You hired the nine women and threw out the calendar.
Closing thoughts
Let’s revisit the premise of using TypeScript for your LLMs: “but the models are so much better at TypeScript.” This is wrong in two different directions.
First, even granting the premise, it’s the most temporary fact in the discussion. Model capability on smaller-corpus languages only moves one direction, and it’s moving fast. You’d be picking the stack you maintain for a decade to accommodate a tooling deficiency measured in months, and accepting a permanent unsoundness to do it. You’re trading a shrinking weakness for a structural one and calling it pragmatism.
But I don’t actually grant the premise: modern LLMs are already very, very good at writing Haskell. The folk wisdom that they flail outside the JavaScript heartland is a year or two stale. I’ve been using LLMs to build a set of packages that are to my tastes, and many of them are working out to be among the fastest, most correct implementations in the world. For example, I recently built wireform-html, a conformant DOM library in Haskell, with heavy model assistance, and to the best of my knowledge it’s the fastest DOM library in the world, in any language. A conformant DOM must navigate a while gnarly spec: tree mutation semantics, namespace rules, the parsing quirks browsers actually implement, etc.; The model managed to arrive at full spec conformance, relatively quickly, and I was able to spend a good chunk time guiding it through a series of performance improvements that have it on par or faster than existing libraries built in C and Rust. When the types are sound, the model’s wrong turns get rejected immediately and precisely, and the loop converges. The expressive language didn’t slow the AI down. It made the AI’s output trustworthy enough to polish into something I’d actually operate in production.
That inverts the whole argument for defaulting to TypeScript. The case rested on the LLM being so much better at TS that the unsoundness was a price worth paying. But the LLM isn’t meaningfully better at the thing that matters (producing correct code), and in a sound language it’s arguably better, because it gets a real correction signal instead of a green checkmark it can game. You’d be paying the permanent price for a discount that no longer exists.
A note on Haskell, since I’ve used it as a point of comparison: nothing in this argument is actually about Haskell. It’s my worked example because it’s what I enjoy using / use at work, but the key property you need in a programming language that an LLM model can operate well is soundness plus an expressive type system, and several languages have it. Rust gives you the same machine-checked rejection of whole classes of error, with an even larger and faster-growing corpus the models train on. Scala lets you encode invariants, with all the other benefits of the JVM ecosystem. Idris and the other dependently-typed languages push the ceiling further still. Pick whichever fits your domain and your team. The claim isn’t “use Haskell.” It’s “use a language whose compiler tells the truth, and don’t trade that away to make this year’s autocomplete’s life easier.”
Graham’s programmers couldn’t see up the power continuum because they thought in Blub. The TypeScript trap is subtler and a little sadder: you can see up the continuum perfectly well. You know what soundness buys you. You’re just being told to give it up because the model is supposedly fluent only in the unsound thing, and it’s easier to lower yourself to the tool than to check whether the tool needs you to. But it doesn’t. LLMs will write you a fast, correct, conformant library in a sound, expressive language today, and the compiler will keep it honest in a way no amount of tsc ever will.
So when the question is which language should be the default in a mixed shop, “the one the AI is currently best at” fails twice: it’s the answer that will age the worst, and it isn’t even true. Pick a language that lets you say true things about your program and have a machine actually enforce them.