The TypeScript Feature That Changed How I Write React

Ram Kumar

Ram Kumar

January 5, 20254 min read

The TypeScript Feature That Changed How I Write React

When working with React, TypeScript is more than just a helpful tool; it’s a game-changer. It brings type safety, better developer experience, and clearer code. But one TypeScript feature in particular has fundamentally changed how I approach writing React applications: Discriminated Unions.

This single feature has allowed me to simplify complex component logic, ensure type safety in ways I never thought possible, and create applications that are not only more robust but also easier to maintain. Let’s dive into how discriminated unions have redefined my approach to writing React.

Understanding Discriminated Unions

A discriminated union (sometimes called a tagged union) is a TypeScript feature that combines the power of union types with a common discriminant property. This makes it easy to define and distinguish between multiple related types within a single union.

Here’s an example:

type ButtonVariant =
  | { type: 'primary'; color: 'blue' }
  | { type: 'secondary'; color: 'gray' }
  | { type: 'danger'; color: 'red' };

Here, type is the discriminant property, and TypeScript can infer the correct shape of the object based on its value. If you pass type: 'primary', TypeScript knows the object must also include color: 'blue'.

How Discriminated Unions Changed My React Workflow

1. Simplifying Component Props

Before adopting discriminated unions, I often used large, flat interfaces to define props for React components. While this worked, it became cumbersome when components needed to handle multiple variations of state or behavior.

Now, I use discriminated unions to cleanly define the different states a component might support. For example, let’s say I’m creating a Button component with different variants:

type ButtonProps =
  | { variant: 'primary'; onClick: () => void }
  | { variant: 'secondary'; href: string }
  | { variant: 'disabled'; reason: string };

const Button: React.FC<ButtonProps> = (props) => {
  switch (props.variant) {
    case 'primary':
      return <button onClick={props.onClick}>Primary Button</button>;
    case 'secondary':
      return <a href={props.href}>Secondary Button</a>;
    case 'disabled':
      return <button disabled>{props.reason}</button>;
    default:
      return null;
  }
};

Here, discriminated unions make it easy to enforce logic at both compile-time and runtime. TypeScript ensures that I handle all variants explicitly and prevents invalid combinations of props.

2. Reducing Runtime Errors

One of the challenges of React development is managing state transitions and ensuring that components behave correctly in different scenarios. With discriminated unions, I can represent state transitions explicitly and let TypeScript enforce correctness.

For example, consider a Modal component that can either be open or closed:

type ModalState =
  | { isOpen: true; content: string }
  | { isOpen: false };

type ModalProps = {
  state: ModalState;
};

const Modal: React.FC<ModalProps> = ({ state }) => {
  if (state.isOpen) {
    return <div className="modal">{state.content}</div>;
  }
  return null;
};

Here, TypeScript ensures that if the modal is open, the content property must be present. If the modal is closed, accessing content would result in a compile-time error. This eliminates an entire class of runtime bugs.

3. Enhancing Component Reusability

By using discriminated unions, I can create highly reusable components that adapt their behavior based on clear, type-safe inputs. For instance, a Notification component might handle success, warning, and error states:

typescriptCopyEdit

type NotificationProps =
  | { status: 'success'; message: string }
  | { status: 'warning'; message: string; retry: () => void }
  | { status: 'error'; message: string; errorCode: number };

const Notification: React.FC<NotificationProps> = (props) => {
  switch (props.status) {
    case 'success':
      return <div className="notification success">{props.message}</div>;
    case 'warning':
      return (
        <div className="notification warning">
          {props.message}
          <button onClick={props.retry}>Retry</button>
        </div>
      );
    case 'error':
      return (
        <div className="notification error">
          {props.message} (Error Code: {props.errorCode})
        </div>
      );
  }
};

Each variant of the Notification component has its own specific props, and TypeScript ensures I don’t accidentally pass invalid combinations. This makes the component easier to use, reduces bugs, and provides a better developer experience.

The Benefits of Using Discriminated Unions

The power of discriminated unions lies in their ability to represent complex scenarios in a concise, type-safe way. Here are a few key benefits I’ve experienced:

Clearer Code: Components are easier to read and understand because the logic for each variant is explicit.

Fewer Bugs: TypeScript catches invalid state transitions and prop combinations before they even reach the browser.

Better Developer Experience: Autocomplete and inline documentation in IDEs make working with discriminated unions a joy.

Scalability: As applications grow, discriminated unions make it easier to manage complexity without sacrificing type safety.

Conclusion

Discriminated unions are the TypeScript feature that changed how I write React. They allow me to represent complex component logic in a clean, maintainable way while ensuring type safety and reducing runtime errors. Whether you’re building a simple component or a large-scale application, incorporating discriminated unions into your workflow will make your codebase more robust and easier to work with.

If you haven’t already embraced this feature, I highly recommend giving it a try. You might just find, like I did, that it fundamentally changes how you approach React development with TypeScript.

Previous: Understanding React.memo: When, Why, and How to Use It
Next: Lazy Loading in Angular: Optimize Your Application’s Performance