Type-Safe React: Harnessing the Power of Discriminated Unions
Tags: react, typescript, frontend, solidprinciples
Published: 2024-05-26 14:01:46

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.

```typescript
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:

```typescript
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.

```typescript
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:

```typescript
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?

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!