You spend an entire afternoon adding types to your project. Every variable has an interface, every function has explicit return types, and your edi...
For further actions, you may consider blocking this person and/or reporting abuse
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.
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!
The
as ApiResponseexample 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 attoUpperCaseinstead of the boundary that let bad data in. Parsing at the edge with something likeschema.safeParse(await res.json())and failing loudly there gives you an error that names the actual problem. One small addition:noUncheckedIndexedAccesscatches a good share of the "cannot read properties of undefined" crashes that come from arrays and records rather than API responses.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!
The
as ApiResponseexample 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
nullcome 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.
Absolutely, this is exactly the kind of issue I had in mind with the
as ApiResponseexample. 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!The part about
asis 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 asApiResponse, 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
unknownas more than just a safer alternative toany. 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.
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!
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.
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!
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!
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.
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!****
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.
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!
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.
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.
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.
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!
nice typescript breakdown
Thanks 🙂
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!