Why I’m Frustrated with TypeScript: A Developer’s Rant
TypeScript promises type safety and better tooling, but years of real-world use tell a different story. This post breaks down why TypeScript isn’t a true static type system, how its configs, boilerplate, and fragile tooling create more pain than safety, and why developers who love strong typing keep using it anyway. Learn where TypeScript falls short, what Bun improves, and what a better typed language for the web could look like.

Robert Ponsford
•
Aug 5, 2025
TypeScript is everywhere these days. It promises type safety, better tooling, and a more robust JavaScript experience. And honestly, I want to love it, I’m a huge fan of static typing and the clarity it should bring.
But after years of working with it, I have to say:
TypeScript still feels like a half-baked, overly complex mess.
Here’s why.
TypeScript Isn’t a True Static Type System
TypeScript pretends to be a real statically typed language, but in reality, it’s just an augmented type checker layered over JavaScript. It doesn’t enforce types at runtime, only at compile time.
🔍 Real Type Systems vs. TypeScript
// Go (compile-time + runtime safety)
func divide(a, b int) int {
return a / b // Compiler prevents float, string, etc.
}
// TypeScript (compile-time only)
function divide(a: number, b: number): number {
return a / b;
}
divide("hello", "world"); // Will throw at runtime if unchecked
Even with strict flags, you can still bypass the system with any, as, or incorrect inputs from JS land. And guess what? It won’t stop you.
Type Wizardry
Ever seen this?
type NestedKeys<T> = {
[K in keyof T]: T[K] extends object ? K | `${K}.${NestedKeys<T[K]>}` : K
}[keyof T];
Useful? Sometimes. Understandable? Rarely. Debuggable? Not even remotely.
TypeScript allows extremely complex types but provides limited tools to introspect or debug them.
Types That Don’t Actually Enforce Anything
Many TypeScript keywords exist only during development. They do not carry into runtime.
readonly is a LIE
type Person = {
readonly name: string;
};
const user: Person = { name: "Alice" };
(user as any).name = "Bob"; // No runtime error
You feel like you’re writing safe, immutable code, but unless you freeze it manually, it’s just an illusion.
Compare with:
// Go - you literally cannot mutate unless allowed
const name string = "Alice"
// name = "Bob" // compile-time error
The Nightmare That Is tsconfig.json
tsconfig.json is a JSON-based config file with dozens of flags, many of which conflict or change meaning across versions.
One Wrong Flag and Everything Explodes
{
"module": "esnext",
"target": "es2022",
"moduleResolution": "node"
}
Want top-level await? Oh, now you need "module": "esnext" and to be careful with "isolatedModules". Forget one? Your build breaks.
TypeScript doesn’t just punish bad configs, it punishes slightly imperfect ones too.
TypeScript Forces Boilerplate and Bloat
In trying to satisfy both JavaScript and typing, TypeScript ends up requiring far too much work for what should be simple tasks.
Basic Validation Requires Mental Gymnastics
interface User {
name: string;
age: number;
}
function isUser(obj: any): obj is User {
return typeof obj.name === "string" && typeof obj.age === "number";
}
This kind of runtime validation is manual and repetitive, yet essential because types don’t exist at runtime.
Compare with Go:
type User struct {
Name string
Age int
}
func processUser(u User) {
fmt.Println(u.Name)
}
Go’s compiler enforces structure. TypeScript makes you enforce it manually if you care about runtime safety.
Tooling Is Fragile and Unreliable
Despite VSCode’s tight integration with TypeScript, performance tanks in large projects. Type inference breaks. Refactoring complex types often leads to cascading errors in unrelated files.
Type-Related Errors You’ve Probably Seen:
- “Type ‘string | undefined’ is not assignable to type ‘string’”
- “Property ‘X’ does not exist on type ‘never’”
- “Expected 1 arguments, but got 2”
And when the type checker gets confused, your only escape hatches are as any, @ts-ignore, or breaking it into three layers of interfaces just to make inference work again.
Why I Still Use TypeScript (Even Though I Hate It)
Despite all this, TypeScript remains the most widely accepted way to bring type safety into JavaScript codebases.
- It’s not a real type system.
- It’s not sound.
- It lets you lie to it easily.
But it’s better than nothing. That’s its only real strength.
Enter Bun: Fixing the DX, Not the Language
I’ll give credit where it’s due: Bun starts to fix a lot of the TS workflow pain.
- No
tscor Babel, just run.tsfiles directly - Zero-config project bootstrapping
- Lightning fast dev server & runtime
- Better default error messages
Bun makes TS feel like a real language, but the illusion is still there.
TS Still Can’t Do This:
// This will still break in Bun
const user = getDataFromUntrustedSource() as any;
console.log(user.name.toUpperCase()); // Runtime crash if name is undefined
Bun gives you a smoother ride. But it’s still the same flawed engine underneath.
What a Better System Could Look Like
Look, I want to love the concept of TypeScript because I love types. But it’s just so poorly executed.
So here’s my non-serious, maybe-too-aspirational wish list for the better typed language I really want:
Immutability That’s Actually Enforced
Like Go, where immutability is about scope and intent, no tricks or confusing readonly keywords that don’t exist at runtime.
type User struct {
Name string
Age int
}
func updateName(u User, newName string) User {
u.Name = newName // Works only on the copy, safe by design
return u
}
No shadow boxing with mutable objects pretending to be frozen.
Simple, Clear Types Without Mental Gymnastics
Go keeps types straightforward and explicit. No 10-level deep conditional mapped infer nonsense.
type Response struct {
Data interface{}
Error error
}
if res.Error != nil {
handleError(res.Error)
} else {
render(res.Data)
}
I want that clarity on the web.
Runtime Safety Without Extra Libraries
Types that aren’t just compile-time suggestions, but real contracts the runtime checks.
var user User
err := json.Unmarshal(input, &user)
if err != nil {
return err
}
Built-in runtime guarantees without pulling in heavy validation libraries.
Tooling That Just Works
No tsconfig.json nightmares or editor lags. Fast, reliable, and simple.
Designed for the Web, Not Bolted On
A language that doesn’t just compile to JavaScript but runs natively or via WebAssembly with first-class DOM and concurrency support, clean async handling like Go’s goroutines but for the browser.
TL;DR
I want something like Go, but for frontend and full-stack, simple, reliable, with real types that work in runtime and compile time, and tooling that never gets in the way.
Types aren’t just a nice-to-have. They’re a promise. And that promise should be kept, not a patchwork of illusions and workarounds.
Final Thoughts
If you’re a fan of strong typing, be warned:
TypeScript is a complicated, confusing, and contradictory system that solves just enough to remain viable, while frustrating everyone who actually cares about type safety.
I don’t hate TypeScript because I hate types, I hate it because I love types.
I love what it wants to be. I just hate what it is:
- A leaky abstraction.
- A typing bandaid.
- A dream of safety implemented in JavaScript’s broken sandbox.
I’m still using it. But I’m always looking for the day I don’t have to.