Type-Safe React: Harnessing the Power of Discriminated Unions
Ibukunoluwa Popoola
Posted on May 26, 2024
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>;
};
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;
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;
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>;
};
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?
-
Type Safety: Discriminated unions ensure that the type of the
props
object is correctly inferred based on the value of the discriminant property. - Code Clarity: By defining separate interfaces for each possible type, our code becomes more readable and easier to understand.
- 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!
Posted on May 26, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.