Better React Props

publish: February 1, 2024

modified: September 27, 2024

A simple pattern, yet not widely utilized, is discriminated unions. When using React, you can enhance your props by employing discriminated unions. This powerful pattern can be employed in various ways, although I often find it underutilized. In this post, I will demonstrate how to utilize it to improve React props.

Let’s consider the following component:

type Props = {
  onClick: () => void;
};

function Button(props: Props): JSX.Element {
  return <button onClick={props.onClick}>Click me</button>;
}

This Button component receives an onClick function, which is the most common use case. However, what if we want to pass an href prop instead and render an a tag? We could do something like this:

type Props = {
  onClick?: () => void;
  href?: string;
};

function Button(props: Props): JSX.Element {
  if (props.href) {
    return (
      <a href={props.href} onClick={props.onClick}>
        Click me
      </a>
    );
  }

  if (props.onClick) {
    return <button onClick={props.onClick}>Click me</button>;
  }

  return null;
}

While this solution works, it is not ideal. We need to check for the existence of each prop and return the correct element. Additionally, we can pass both the onClick and href props, which is not desirable. To address these concerns, we can utilize discriminated unions. By doing so, we can create a type that only allows one of the props to be passed. Let’s see how that looks:

type Props =
  | {
      type: "button";
      onClick: () => void;
    }
  | {
      type: "link";
      href: string;
    };

// Same button implementation as before
// However, when using this component and passing both props, an error will be raised
<Button type="button" onClick={() => {}} href="/home" />;

This approach provides several benefits:

  1. It ensures that only valid combinations of props are used. You can’t pass both onClick and href props simultaneously.
  2. It provides clear guidance to the user of the component on which props are allowed.
  3. It improves type safety, as TypeScript will catch any incorrect usages at compile-time.

Using Discriminated Unions on React Reducers

Discriminated unions can also be useful when working with reducers in React. They provide a way to handle different actions and their associated payloads in a type-safe manner. Here’s an example:

type State =
  | { status: "pending" }
  | { status: "loading" }
  | { status: "success"; data: Book[] }
  | { status: "error"; error: any };

type Action =
  | { type: "FETCH_PENDING" }
  | { type: "FETCH_LOADING" }
  | { type: "FETCH_SUCCESS"; payload: Book[] }
  | { type: "FETCH_ERROR"; payload: any };

function reducer(state: State, action: Action): State {
  switch (action.type) {
    case "FETCH_PENDING":
      return { status: "pending" };
    case "FETCH_LOADING":
      return { status: "loading" };
    case "FETCH_SUCCESS":
      return { status: "success", data: action.payload };
    case "FETCH_ERROR":
      return { status: "error", error: action.payload };
    default:
      return state;
  }
}

// Usage
const initialState: State = { status: "pending" };
const [state, dispatch] = useReducer(reducer, initialState);

// Dispatch actions
dispatch({ type: "FETCH_PENDING" });
dispatch({ type: "FETCH_LOADING" });
dispatch({ type: "FETCH_SUCCESS", payload: books });
dispatch({ type: "FETCH_ERROR", payload: error });

In this example, we use discriminated unions to represent different states of a fetch operation in the State type. Each state, such as ‘pending’, ‘loading’, ‘success’, or ‘error’, includes additional data.

The Action type defines various actions that can be dispatched to the reducer, representing different state transitions. Each action has a specific type and optional data associated with it.

By using a switch statement, the reducer function handles each action type individually and updates the state accordingly based on the current state and the action received.

Using discriminated unions in this way improves type safety and makes managing state in React applications more straightforward for developers.


The importance of this is garanty of type safety. By utilizing discriminated unions in React props and reducers, we enhance type safety, ensuring that only valid combinations of props are used and that actions and state transitions are handled correctly. TypeScript, with its static type checking, becomes a valuable tool in catching potential errors and providing early feedback during development. With type safety, we can confidently refactor, collaborate, and build robust React applications that are less prone to runtime errors, improving overall development productivity and code quality.

Useful Sources