TypeScript: The Illusion of Structure
It’s been around seven years since I started coding with JavaScript, and for the last two years I’ve been working almost exclusively with TypeScript for backend development. Sometimes relationships just don’t work out — and I’m starting to feel that JavaScript and TypeScript are simply not a good fit for backend systems when projects become serious and complex 🔒
As soon as you try to do anything non-trivial with your ORM, things start getting messy — and it doesn’t really matter whether you’re using TypeORM or Prisma. The moment you need “ORM magic” like sharing transactions across repositories, handling nested soft deletes, or coordinating complex lifecycle behavior, you find yourself fighting the abstractions rather than benefiting from them.
Yes, TypeScript is “typed”, but in reality it’s still dynamically typed at runtime. Once your code is running, you’re basically alone — relying on tools like class-validator or Zod to keep things safe. Error handling isn’t much better: promise rejections give you very little context unless you’ve carefully designed your error model in advance. If you don’t explicitly carry context through your system, debugging becomes an archaeological exercise for your future self 💩
Try introducing more advanced architectural patterns like CQRS or Event Sourcing and it becomes even more painful. There is no real ecosystem support, no established standards, and no mature infrastructure. Most of the time, you feel like you’re the first person trying to build these patterns in the ecosystem — reinventing foundations instead of building on them.
Even the tooling tells a story of fragmentation. Configuring the TypeScript compiler often means dealing with historical baggage: CommonJS vs ESM, legacy module systems, hybrid configs, and confusing interoperability rules — all while writing modern ESM code and hoping the build pipeline behaves the way you expect.
But the most painful part for me is architectural 😱. There is no real modularization model beyond the file. The only true unit of composition is a file. No namespaces. No real packages. No enforced module boundaries. No architectural dependency control. This makes it extremely hard to model large systems cleanly. In languages like Go or Java, modularization is a first-class concept — and architecture is enforced by the compiler, not by discipline alone.
Even in NestJS — the most opinionated backend framework in the ecosystem — the “module” concept isn’t real modularity. It’s just a DI scope. There is no compiler enforcement. Any file can import any other file from any module directly, without declaring dependencies at the module level. So the module system doesn’t actually protect your architecture — it just organizes providers for dependency injection.
Am I the only one complaining about this?
Apparently not. The CTO of Motion wrote a detailed post about their experience maintaining a 2.5 million line TypeScript codebase — and their decision to migrate to .NET. That resonated deeply with me. https://engineering.usemotion.com/moving-off-of-typescript-e7bb1f3ad091
And it’s not just Motion. The CTO of Tracebit — despite being experienced with TypeScript and Python — still chose to start the company on .NET from day one, deliberately building the system in the .NET ecosystem rather than adopting a JavaScript-based backend. https://tracebit.com/blog/why-tracebit-is-written-in-c-sharp
I’m not saying TypeScript is a bad language. It’s a great language for many things. But I don’t believe it’s a good foundation for systems that are serious, long-living, and architecturally complex. It feels undercooked for that role — and I’m not convinced it will ever fully mature into a truly strong backend platform for large-scale systems.
And .NET?
Yeah… I’m starting to seriously consider giving it a real taste ☕️
comments powered by Disqus