← Back to Blogs

Architecting Type-Safe Component Interfaces: Advanced TypeScript Patterns for Senior React Developers

Sun Jun 28 20264 min read

Hey everyone, I’m Prashuk.

I spend most of my day living inside VS Code, mostly working with React, Next.js, and the MERN stack. If you’re like me, you probably hate it when your editor doesn’t give you the right autocomplete or, worse, when a component breaks in production because a prop was "technically" there but was actually undefined.

Lately, I’ve been obsessed with making my TypeScript interfaces work for me, not against me. We’ve all seen those tutorials that explain Generics using Array<T>, but in the real world, we’re building buttons, modals, and complex forms.

Here is how I’ve been architecting component interfaces lately to keep things clean and type-safe.


1. The "Generic Component" Trap

In one of my recent projects—a SaaS dashboard—I had to build a Table component. Initially, I just passed data: any[]. Bad move. I lost all type safety inside the row rendering.

What worked for me was using Generics properly. Instead of guessing what the data looks like, let the component infer it.

interface TableProps<T> {
  items: T[];
  renderRow: (item: T) => React.ReactNode;
}

function Table<T>({ items, renderRow }: TableProps<T>) {
  return <table>{items.map(renderRow)}</table>;
}

Now, when I use <Table items={users} />, TypeScript knows exactly what user is inside renderRow. It sounds simple, but it saves so much time during debugging.

2. Handling "Polymorphic" Components

We’ve all built a Button component that sometimes needs to be a Link (<a>) or a React Router <Link>.

I ran into this issue when building a UI library for a client. If I just added href as an optional prop, I’d still have to handle the logic manually, and the types were messy.

The "Advanced" way is using an as prop. But honestly? Don't over-engineer it if you don't have to. For most of my projects, I use a discriminated union for the props. If href exists, it must be an anchor. If not, it’s a button. It keeps the UI clean and prevents me from accidentally passing a type="submit" to an <a> tag.

3. Discriminated Unions over Optional Props

This is my biggest pet peeve. Stop doing this: interface StatusProps { isLoading?: boolean; data?: string; error?: string; }

Why? Because technically, TypeScript thinks it's okay to have isLoading: true AND data: "Success". In reality, that never happens.

What I do instead: I define the state clearly.

type State = 
  | { status: 'loading' }
  | { status: 'success'; data: string }
  | { status: 'error'; message: string };

// In the component:
if (props.status === 'success') {
  console.log(props.data); // TS knows data exists here!
}

I used this pattern for a crypto-tracking app I was building, and it eliminated about 90% of my "undefined is not a function" errors.

4. ComponentPropsWithoutRef is your friend

When you’re building a wrapper around an input or a button, don't manually type out className, onClick, onBlur, etc. It’s a waste of time.

I always extend React.ComponentPropsWithoutRef<'button'>. It makes the component feel like a native HTML element. Plus, if React updates their types, your component gets the updates for free.

One tip: If you need to forward a ref (like for Framer Motion or a focus hook), use ComponentPropsWithRef. But keep it simple—only use it when you actually need the ref.

5. My "Practical" Best Practices

  • Export your interfaces: You’ll almost always need them in a parent component or a test file. Just export them from the start.
  • Avoid any like the plague: I’d rather write a complex generic than use any. If you use any, you’re just writing JavaScript with extra steps.
  • Focus on the DX (Developer Experience): If your component is hard to use because the types are too complex, simplify them. Types should help you code faster, not slow you down.
  • UI/Performance matters: Remember that TypeScript is stripped away at runtime. It doesn't fix a heavy re-render. Always check your React.memo usage alongside your types.

Final Thoughts

Moving from a mid-level to a senior dev usually means writing less code but making that code much smarter. Thinking about how your props interact before you even write the return statement is a game changer.

In my experience, the best "advanced" pattern is the one that your teammates can actually understand without needing a PhD in Type Theory. Keep it practical, keep it clean.

What’s a pattern you’ve been using lately? Let me know, I’m always looking to clean up my boilerplates.

— Prashuk