DEV Community

Cover image for The "Safety Check" Habit: Why Your TypeScript Code Still Bleeds in Production
Bishoy Bishai
Bishoy Bishai

Posted on • Originally published at bishoy-bishai.github.io

The "Safety Check" Habit: Why Your TypeScript Code Still Bleeds in Production

Let’s be honest. We’ve all been there. It’s 2 AM, you’re staring at a Sentry report or a bug ticket that says: Uncaught TypeError: Cannot read properties of undefined (reading 'map').

You feel a mix of confusion and betrayal. "But I’m using TypeScript!" you tell the screen. "How did this even compile?"

Here is the uncomfortable truth: TypeScript is not a magic shield; it’s a documentation tool that happens to have a compiler. If you use it like a "fancy comment system" rather than a "strict architecture guardian," you’re just building a more expensive version of the same house of cards.

The biggest bugs don't come from a lack of knowledge. They come from bad habits.

Today, we’re going to build five tiny, non-negotiable habits that will stop those 3 AM production fires.


📖 The Setup: The "False Security" Trap

The biggest mistake I see mid-level developers make is trusting any or implicit inference too much. When I was migrating the Tajawal flights app from AngularJS to React, we were moving millions of dollars in revenue. If I had relied on "guessing" what the API returned, I wouldn't be sitting here writing this today. I’d be in a different career entirely.

The goal isn't just to make the red squiggly lines go away. The goal is to make the code impossible to break.


Habit 1: The "No-Ghost" Prop Strategy

Stop letting optional props be "ghosts." If a component can receive null, you must scream it from the rooftops of your interfaces.

The Breakdown

I’ve seen this a thousand times: A UserProfile component expects a user object. The dev types it as user: User. But what happens when the API is still loading? Or if the user doesn't exist? The app crashes because user was undefined at runtime, but TypeScript thought it was always there.

The Match Report

// ❌ The "Optimistic" Way (Dangerous)
interface Props {
  user: User; // This is a lie if data is fetching
}

// ✅ The "Bishoy Approved" Habit
interface UserProfileProps {
  user: User | null; // Honesty is the best policy
  isLoading: boolean;
}

const UserProfile = ({ user, isLoading }: UserProfileProps) => {
  if (isLoading) return <LoadingSpinner />;

  // TypeScript forces you to handle this. You can't skip it.
  if (!user) return <EmptyState message="User not found" />;

  return <div>{user.name}</div>;
};

Enter fullscreen mode Exit fullscreen mode

Why this works: By adding | null, you are forcing your future self to handle the empty state. You are coding with empathy for the user who might have a slow connection.


Habit 2: Discriminated Unions (The "Impossible State" Killer)

In 'Surrounded by AI,' I talk about how machines are "average machines". They choose the most probable path. But as developers, we have to handle the extremes.

The Setup

If you have a state that looks like this:
{ isLoading: boolean, data: string[], error: string }

You have created a monster. It is mathematically possible to have isLoading: true AND data: [...] AND error: "Boom". Which one do you show?

The Solution

Use a Discriminated Union. It’s the single most powerful feature in TypeScript for React.

type RequestState = 
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: string[] }
  | { status: 'error'; message: string };

// Now, in your component:
if (state.status === 'success') {
  // state.data is guaranteed here. state.message doesn't exist.
  return <List items={state.data} />;
}

Enter fullscreen mode Exit fullscreen mode

You have just deleted 50% of your potential bugs by making "impossible states" unrepresentable in code.


Habit 3: The useRef Null-Guard

useRef is the "wild west" of React. If you don't type it correctly, you're basically using jQuery again.

Most tutorials show const ref = useRef(). This defaults to any. In a production app, we handle sensitive UI elements. We can't afford a null pointer exception because a ref wasn't attached yet.

The Habit

Always initialize with null and provide the explicit HTML element type.

// ✅ Explicit and safe
const inputRef = useRef<HTMLInputElement>(null);

const handleFocus = () => {
  // TypeScript will NOT let you call .focus() without this check
  if (inputRef.current) {
    inputRef.current.focus();
  }
};

Enter fullscreen mode Exit fullscreen mode

Habit 4: Precise Event Handler Typing

Don't use any for events. Just don't. It’s a Red Card behavior.

The Breakdown

When you write onChange={(e: any) => ...}, you lose the ability to know what e.target actually is. Is it an input? A select? A textarea?

The Match Report

// ✅ Use React's built-in types
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
  console.log(e.target.value); // Full autocomplete and safety
};

const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
  e.preventDefault();
};

Enter fullscreen mode Exit fullscreen mode

Pro Tip: If you're not sure what the type is, hover over the onChange or onClick prop in the JSX. TypeScript will literally tell you the type you need to copy. I wish someone had told me this in 2015.


Habit 5: The Partial<T> Safety Net

When updating state (especially in Redux or Context), we often want to update just one field.

The Setup

If you have a User object with 20 fields, you don't want to pass all 20 fields to an updateUser function.

The Habit

Use Partial<T> to tell TypeScript: "I'm sending some of these fields, but they must still match the original shapes."

interface User {
  id: string;
  name: string;
  email: string;
  theme: 'dark' | 'light';
}

const updateUserSettings = (updates: Partial<User>) => {
  // 'updates' can be { theme: 'dark' } 
  // but it CANNOT be { theme: 'blue' }
};

Enter fullscreen mode Exit fullscreen mode

⚖️ The VAR Section (Objections)

"This takes so much longer to write!"
Actually, no. It takes longer to type, but it takes 10x less time to debug. You are moving the "discovery of bugs" from the user's browser to your IDE. That is a massive ROI.

"My team doesn't use these patterns."
Be the Maestro. Start using them in your PRs. When your features have zero regressions, they will start asking how you did it.

"What about external APIs that return bad data?"
TypeScript only protects you inside your app. For the borders, use a library like Zod to validate data at the gate. As I wrote in the book, "We buy the scars, not the wood". Your code should show it knows the world is messy.


📖 The Unfinished Chapter

Even after 15 years, I still struggle with complex Generic types sometimes. I still occasionally reach for an any when I’m tired. But the habit is what brings me back.

What are you struggling with? Are you still using any as a "temporary" fix that ends up staying for 2 years? Confess it in the comments. We’re all learning.


Bookmarks

"TypeScript is a conversation between you and the compiler about the future."


✨ Let's keep the conversation going!

If you found this interesting, I'd love for you to check out more of my work or just drop in to say hello.

✍️ Read more on my blog: bishoy-bishai.github.io

Let's chat on LinkedIn: linkedin.com/in/bishoybishai

📘 Curious about AI?: You can also check out my book: Surrounded by AI


Top comments (0)