Type-Safe React: Harnessing the Power of Discriminated Unions

gboladetrue

Ibukunoluwa Popoola

Posted on May 26, 2024

Type-Safe React: Harnessing the Power of Discriminated Unions

As front-end developers, we've all been there - in a situation where we want to pass a prop to a child component, but only if another prop is present. It's a common situation that can lead to messy, hard-to-maintain code. But fear not, dear reader, for there's a solution that can simplify your code and make it more type-safe: discriminated unions.

Conditional Props

Imagine you're building a Button component that can be either a primary or secondary button. You want to pass a label prop to the component, but only if the variant prop is set to "primary". If variant is "secondary", you don't want to pass the label prop.

interface ButtonProps {
  variant: 'primary' | 'secondary';
  label?: string;
}

const Button = ({ variant, label }: ButtonProps & React.ButtonHTMLAttributes<HTMLButtonElement>) => {
  return <button {...props}>{variant === 'primary' ? label : ''}</button>;
};
Enter fullscreen mode Exit fullscreen mode

This implementation has some issues:

The label prop is optional, which can lead to undefined errors if it's not provided when variant is "primary".
The component has to perform runtime checks to ensure the label prop is present when variant is "primary".

Enter Discriminated Unions:

Discriminated unions, also known as tagged unions, are a powerful feature in TypeScript that can help us solve this problem. A discriminated union is a type that can be one of several types, depending on the value of a specific property, called the discriminant.

Let's redefine our ButtonProps interface using a discriminated union:

interface PrimaryButtonProps {
  variant: 'primary';
  label: string;
}

interface SecondaryButtonProps {
  variant: 'secondary';
}

type ButtonProps = PrimaryButtonProps | SecondaryButtonProps;
Enter fullscreen mode Exit fullscreen mode

Here, we've defined two separate interfaces: PrimaryButtonProps and SecondaryButtonProps. By defining ButtonProps as a union type, we're saying that a ButtonProps object can have either the shape of a PrimaryButtonProps object or a SecondaryButtonProps object depending on the variant property, which is the discriminant. It is important to note that whatever is used as the discriminant is required for all separate interfaces when applying the concept of discriminated unions. So if we had a third button variant, say "tertiary", the interface TertiaryButtonProps must include the variant prop.

interface PrimaryButtonProps {
  variant: 'primary';
  label: string;
}

interface SecondaryButtonProps {
  variant: 'secondary';
}

// Wrong Implementation
interface TertiaryButtonProps {
  // missing required `variant` prop
  tertiaryProp?: any;
}

// Correct Implementation
interface TertiaryButtonProps {
  variant: 'tertiary'; // Required prop
  tertiaryProp?: any;
}

type ButtonProps = PrimaryButtonProps | SecondaryButtonProps | TertiaryButtonProps;
Enter fullscreen mode Exit fullscreen mode

Now, let's update our Button component to use this new ButtonProps type:

const Button = (props: ButtonProps & React.ButtonHTMLAttributes<HTMLButtonElement>) => {
  const { variant } = props; 
  return <button {...props}>{variant === 'primary' ? props.label : 'Secondary Button'}</button>;
};
Enter fullscreen mode Exit fullscreen mode

With this implementation, TypeScript will ensure that when variant is primary, the label prop is always present. If variant is secondary, the label prop is absent. The discriminated union has helped us handle a conditional prop that depends on the value of another prop.

Here are some other common use cases for discriminated unions in React development:

  • Form Validation: When building forms, we often need to validate user input based on the type of input field. Discriminated unions can help us define separate validation rules for different input types.
  • Modals: When building modals, we might want to pass different props depending on the type of modal (e.g. error modal, success modal, etc.).
  • Accordion Components: Accordion components often require different props depending on whether the accordion item is expanded or collapsed.
  • Responsive Design: We might want to pass different props to a component based on the screen size or device type when building responsive designs.

So, what makes discriminated unions so useful?

  1. Type Safety: Discriminated unions ensure that the type of the props object is correctly inferred based on the value of the discriminant property.
  2. Code Clarity: By defining separate interfaces for each possible type, our code becomes more readable and easier to understand.
  3. Fewer Runtime Checks: With discriminated unions, we can eliminate runtime checks and ensure that our code is correct at compile time.

Conclusion

Discriminated unions are a game-changer in the TypeScript toolbox, empowering you to write more robust, type-safe, and maintainable React components. By harnessing their power, you can ensure consistency and stability as your applications scale. Next time you're faced with dependent conditional props, don't hesitate to reach for a discriminated union - the ultimate problem-solver for a more predictable and efficient codebase.

I hope this post has inspired you to explore the world of discriminated unions in React with TypeScript. Happy coding!

💖 💪 🙅 🚩
gboladetrue
Ibukunoluwa Popoola

Posted on May 26, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related