Back to Blog List

My Problem with Typescript (And Javascript)

Published | 4 min read

I love Typescript. Types are great. I use it basically every day, I use it at work. All in all, I need my types. Smoothless (when it works) intellisence, code documenting itself, and for all the good parts that we love about Typescript. But now, I just want to talk about what I found problematic with it.

For me, the biggest issue I face with Typescript (and by neccessity, Javascript) is what’s known as Happy Path Blindness. The concept itself is not related to Typescript, it’s rather a general UX concept.

Happy Path Blindness?

It’s the tendency to focus exclusively on the ideal scenario where everything goes according to plan. It’s when we design, code, or think about systems assuming that users will always input valid data, networks will never fail, APIs will always return expected responses, and external dependencies will work flawlessly. We assume we live in an ideal world. We become blind to the countless ways things can AND WILL go wrong.

What can go wrong will go wrong.

The Language Design Problem

But here’s the thing, I believe that Typescript (and by neccessity, JavaScript) makes this cognitive bias significantly worse at the language design level. The language actively encourages happy path thinking through several fundamental design decisions that make error handling feel like an afterthought rather than a first-class concern.

Consider this example:

async function getUser(id: string): Promise<User> {
  const response = await fetch(`/api/users/${id}`);
  const user = await response.json();
  return user;
}

This looks clean and type-safe, but it’s a happy path disaster waiting to happen. The function signature promises it will return a User, but what about network failures? What if the API returns a 404? What if the response isn’t valid JSON? What if the JSON doesn’t match our User type?

The language design makes the happy path feel natural and correct, while proper error handling feels verbose and headache. If we tried to make it safer, it would look like this:

async function getUserSafely(id: string): Promise<User | null> {
  try {
    const response = await fetch(`/api/users/${id}`);
    if (!response.ok) {
      return null; // or throw, or return an error object
    }
    const data = await response.json();
    // Still not validating that data actually matches User type...
    return data as User;
  } catch (error) {
    // Network error, JSON parsing error, etc.
    return null;
  }
}

The “safe” version is longer, harder to read, and still doesn’t handle all edge cases properly. Not to mention the try catch headache and its long discussed problems. The language rewards us for ignoring potential failures and punishes us for being thorough.

I’m not in the verge of giving solutions to this here, but if you’re curoious to know more about this problem, check out this video by Theo.

Anyway, this isn’t just about async operations. Even basic property access exhibits this problem:

function getUserEmail(user: User): string {
  return user.profile.email; // What if profile is null/undefined?
}

While Typescript has introduced optional chaining (user.profile?.email) and nullish coalescing, these are band-aid solutions to a deeper architectural issue. The default behavior is still to assume success, and safety requires extra syntax and mental overhead.

The Result? Systematic Underestimation of Failure

This language-level bias toward happy paths has real consequences. It trains us to systematically underestimate how often things go wrong in production. We write code that looks type-safe and feels robust in our development environment, but crumbles when faced with the chaos of real-world usage.

The type system gives us a false sense of security. Yes, we know that user.profile.email is a string, but only if we successfully fetched the user, and only if the profile exists, and only if the API actually returned what we expected. The types document the happy path beautifully, but they’re silent about the thousand ways that path can be disrupted.

Finally

I had this phase where I was curious about Rust, shoutout to Ahmed Farghal and his amazing videos, by the way. One thing that I really loved was how Rust basically forces you to think about failure from day one. Those Result types? They’re not just nice to have, they’re mandatory. You have to deal with the possibility that things might not work.

That’s why there’ve been some patterns out there applying the Rust way in Typescript. I myself applied that in my job and till now I assume it works pretty well!