DEV Community

Cover image for Why Your TypeScript Code Still Crashes in Production

Why Your TypeScript Code Still Crashes in Production

S M Tahosin on October 06, 2026

You spend an entire afternoon adding types to your project. Every variable has an interface, every function has explicit return types, and your edi...
Collapse
 
pengeszikra profile image
Peter Vivo •

Very important explanation about TypeScript! I created a JS/TS/TSX terminal with online mentor and I can immediatley able to check your example in TiyF. Can I invite as contributor that project? Because the AI mentor knowledge in this moment is very limited, and I think you will be fine for improving.

Collapse
 
smtahosin profile image
S M Tahosin •

Thank you Peter! Really glad you found the examples useful. And yes, absolutely, I’d be happy to contribute to the project. Being able to test these examples directly in a JS/TS/TSX environment with an AI mentor sounds like a really interesting idea. I’d love to see how TiyF works and help improve the TypeScript-related knowledge where I can. Thanks for the invitation!

Collapse
 
amorizz profile image
Amorizz •

The as ApiResponse example is the one that keeps showing up in real stack traces. A cast is a promise to the compiler, not a check, so the crash lands a few functions away from the fetch and the trace points at toUpperCase instead of the boundary that let bad data in. Parsing at the edge with something like schema.safeParse(await res.json()) and failing loudly there gives you an error that names the actual problem. One small addition: noUncheckedIndexedAccess catches a good share of the "cannot read properties of undefined" crashes that come from arrays and records rather than API responses.

Collapse
 
smtahosin profile image
S M Tahosin •

Yes, that’s a really good point about where the error actually appears. The dangerous part of "as ApiResponse" is not just that it can be wrong, but that the wrong assumption can travel through several functions before finally exploding somewhere like "toUpperCase()". Validating at the boundary makes the failure much more meaningful because you know exactly where the bad data entered the system. And "noUncheckedIndexedAccess" is definitely another useful layer for catching a different class of "undefined" bugs. Great addition!

Collapse
 
pavel_kkkkazantsev profile image
Pavel Kazantsev •

The as ApiResponse example is probably the one I've seen most in real projects. TypeScript just trusts you there, so the API can return basically anything 😄

I've had null come back where an object was expected, fields change without warning, and TS still happily tells you everything is fine.

That's why I prefer validating it once when it comes in. Then the rest of the code can just assume the data is actually what the types say it is.

Collapse
 
smtahosin profile image
S M Tahosin •

Absolutely, this is exactly the kind of issue I had in mind with the as ApiResponse example. The compiler trusts the assertion, but the runtime data can still be completely different from what we expect. I really like the approach of validating once at the boundary and then letting the rest of the application work with trusted data. It keeps the validation logic in one place and makes the rest of the code much easier to reason about. Thanks for sharing the real-world experience!

Collapse
 
sinarezaei profile image
Sina Rezaei •

The part about as is probably the one I would emphasize the most.

A type assertion can make a boundary look safer than it actually is. Once response.json() is asserted as ApiResponse, the rest of the code reads as if the contract has already been verified, even though nothing checked the payload at runtime.

That’s why I tend to think of unknown as more than just a safer alternative to any. It makes the trust boundary visible in the code and forces validation or narrowing before the data enters the typed part of the application.

The interesting part is that this also changes where you put the complexity. Instead of spreading defensive checks throughout the application, you can validate once at the boundary and keep the internal domain model strongly typed.

For production systems, that separation between untrusted input and trusted application state is probably more important than simply having strict TypeScript enabled.

Collapse
 
smtahosin profile image
S M Tahosin •

Exactly. I think "as" is one of those TypeScript features that feels helpful at first, but can quietly hide the actual trust boundary. I really like your point about using "unknown" to make that boundary visible. Once the data is validated at the edge, the rest of the application becomes much easier to reason about instead of having defensive checks scattered everywhere. That separation between untrusted input and trusted domain state is something I’ve started appreciating more as well. Thanks for the thoughtful comment!

Collapse
 
alexcodebytes profile image
Oleksandr •

This is the classic 'Type Safety Illusion' trap. Developers forget that TypeScript is strictly a compile-time linter. It vanishes at runtime. If you're blindly casting API responses with as MyInterface without running a validation layer like Zod or ArkType, you're just shifting the runtime crash down the execution stack. TS guarantees type compliance for your code, not for the untrusted data entering your system infrastructure.

Collapse
 
smtahosin profile image
S M Tahosin •

Exactly. “Type Safety Illusion” is a great way to describe it. TypeScript can give us a lot of confidence inside our own code, but that confidence shouldn’t automatically extend to data coming from outside the application. "as MyInterface" can make the code look strongly typed while the actual runtime value is still completely untrusted. Putting a validation layer such as Zod or ArkType at that boundary makes the mental model much more honest. Really appreciate the comment!

Collapse
 
alexcodebytes profile image
Oleksandr •

as MyInterface" makes the code look strongly typed while the actual runtime value is still completely untrusted. This is the exact core of the issue, Tahosin. It’s basically code-level gaslighting. Adding a parser layer like Zod at the network boundaries changes the architecture from "guessing" to "guaranteeing". It makes your TypeScript definitions mean something because they are backed by deterministic runtime assertions. Absolutely spot on addition to the point!

Collapse
 
alexcodebytes profile image
Oleksandr •

Spot on. Types don't exist in the JavaScript runtime environment. If your system boundaries aren't strictly checked and sanitized at the API layer, static types are just expensive, high-effort documentation. Great write-up for beginners.

Collapse
 
smtahosin profile image
S M Tahosin •

Exactly. That’s probably the most important mental model behind the whole article. TypeScript can make our assumptions very clear to the compiler, but those types disappear when the code actually runs. So if the API boundary is not trusted, the types can give us confidence without actually guaranteeing anything about the incoming data. Runtime validation is what closes that gap. Really appreciate you adding this point to the discussion!****

Collapse
 
marcusykim profile image
Marcus Kim •

The RequestState example highlights one additional product decision: whether a failed refresh should hide data the user already had. For a dashboard I'd often keep the last good result visible and model refreshing and refresh_error as explicit states carrying that data. The union still rules out accidental combinations, but the allowed states now reflect the actual UI. I'd test the sequence success -> refresh -> failure, not only the first fetch.

Collapse
 
smtahosin profile image
S M Tahosin •

I really like this distinction. My example was mainly focused on making impossible states unrepresentable, but real UI state often needs to preserve previous data while a refresh is happening. In that case, modeling something like "refreshing" or "refresh_error" while carrying the last successful data makes much more sense. The success → refresh → failure sequence is also a great testing scenario because that’s where a lot of these state-modeling decisions become visible. Thanks for taking the example one step further!

Collapse
 
anthony910609 profile image
Anthony Allen •

Really good explanation, especially the part about "unknown" vs "any" and how TypeScript types disappear at runtime. That’s an easy thing to forget when working with API responses. The discriminated union example is also a great practical pattern.

Collapse
 
smtahosin profile image
S M Tahosin •

Thank you! "unknown" vs "any" was one of the concepts I really wanted to highlight because they can look similar when you first encounter them, but they lead to very different levels of safety. And the runtime type erasure part is especially important when dealing with API responses. I’m also glad the discriminated union example stood out. It’s one of those patterns that looks simple but can make application state much easier to reason about.

Collapse
 
johnanderson55 profile image
John Anderson •

The part about trusting TypeScript at runtime is probably one of the easiest mistakes to make when you're starting out. It’s tempting to think that once something is typed as a string or an interface, the runtime will somehow enforce it too. The distinction between compile-time safety and validating data coming from APIs or users is something that really clicks only after you’ve seen it cause a real bug.

Collapse
 
smtahosin profile image
S M Tahosin •

Absolutely. I think this is one of those concepts that is easy to understand theoretically but much harder to truly internalize until you see a real runtime bug caused by it. When you’re starting with TypeScript, it’s natural to assume that declaring something as "string" or "User" somehow makes the runtime enforce it. Understanding the difference between compile-time checking and runtime validation is probably one of the biggest mental shifts when moving from “using TypeScript” to actually understanding how TypeScript works. Thanks for sharing this!

Collapse
 
jasmijn0177 profile image
Alejandro Yamada •

nice typescript breakdown

Collapse
 
smtahosin profile image
S M Tahosin •

Thanks 🙂

Collapse
 
alexcodebytes profile image
Oleksandr •

AI is single-player. Real work is multiplayer" is one of the hardest-hitting value propositions I've read all sprint. The friction of copy-pasting LLM responses back and forth into Slack or Teams just to get code reviews or design feedback is a massive context-switching tax. Creating a shared execution space where agents read the same files and mail as the teammates totally optimizes the team's overall throughput. Smashed it with this concept, congrats on the launch!