Skip to Content
All posts

Type Safety in TypeScript: When Less is More

 — #TypeScript#Web Development#Type Systems#Best Practices

I spent yesterday refactoring a React component, and I realized something: the most rigid type definitions aren't always the best ones.

The Type Trap

You know the pattern. Someone writes a type like this:

type UserResponse = {
  data: {
    user: {
      id: string;
      profile: {
        name: string;
        email: string;
        preferences: {
          notifications: boolean;
          theme: 'light' | 'dark' | 'auto';
          language: 'en' | 'es' | 'fr' | 'de';
        };
      };
    };
  };
  meta: {
    timestamp: number;
    requestId: string;
  };
};

Then three months later, the API adds a new field. You update the type. Then it deprecates one. Update again. Before you know it, you're spending more time massaging types than shipping features.

Type Safety ≠ Overly Strict Typing

Here's the thing: type safety is about catching real bugs, not about achieving type perfection.

Real bugs are things like:

  • Calling .toUpperCase() on something that might be null
  • Passing a string where a number is expected
  • Accessing properties that don't exist on an object

Real bugs are NOT:

  • A field that could change in the API but probably won't
  • Deeply nested objects that might have optional properties at the leaf level
  • Discriminated unions that cover 99% of cases but not that one edge case from 2019

What I Actually Do Now

I use a pragmatic approach: types should be as strict as necessary, but no stricter.

// ❌ Over-engineered
type StrictUser = {
  id: string;
  profile: {
    name: string;
    email: string;
    preferences: {
      notifications: boolean;
      theme: 'light' | 'dark' | 'auto';
    };
  };
};
 
// ✅ Pragmatic
type User = {
  id: string;
  name: string;
  email: string;
  preferences?: Record<string, unknown>;
};

The second one:

  • Catches the important stuff (missing id, name, email)
  • Lets you iterate on preferences without constant type updates
  • Still gives you autocomplete for known fields
  • Isn't a maintenance burden

When Do You Actually Need Strict Types?

Strict types matter in specific places:

  1. Public API contracts — If you're shipping a library, your types are part of your contract. Make them strict.
  2. Critical business logic — If a single field affects payment, billing, or security, nail it down.
  3. Form validation — Where precision matters, types should reflect that.
  4. Internal data transforms — Within a utility, be strict to catch transform bugs.

But for most application code? Strict types are a luxury you can't always afford.

The Real Win

TypeScript's magic isn't that it catches everything. It's that it catches enough without getting in your way.

Every hour you spend overthinking your types is an hour you're not:

  • Building features
  • Talking to users
  • Fixing real bugs
  • Learning something new

I'd rather ship a feature with Record<string, unknown> in one place and zero production bugs than spend three days perfecting the type and shipping two weeks late.


The takeaway: Type safety is a tool for confidence, not a sport for perfectionism. Use it where it matters. Be pragmatic elsewhere. Ship.