Written by human
This article is proudly written by Danijel Vincijanović.
No content was generated by AI.

Better React Prop Types with Discriminated Union

November 15, 20223 min read
Coding
Photo by Mohammad Rahmani on Unsplash

For a well-described API endpoint, we don't need to go to the implementation level to understand which parameters to provide. The endpoint documentation should explain everything. The same applies to React components. Developers should know how to use components without checking implementation details.

When designing a React component, think about how other developers will use the component. This is especially important for design system components that are used across the whole system.

Imagine that we have a Button component that looks like this. The component will render either <button> or <a> element, depending on the props provided.

1type Props = {
2 href?: string
3 target?: string
4 onClick?: VoidFunction
5}
6
7const Button = (props: Props) => {
8 if (props.href && props.target) {
9 return (
10 <a href={props.href} target={props.target}>
11 Click Me!
12 </a>
13 )
14 }
15
16 if (props.onClick) {
17 return <button onClick={props.onClick}>Click Me!</button>
18 }
19
20 throw new Error("You should provide either href and target or onClick prop.")
21}

This is a trivial example, but we already have some questions when using the component. Which props to provide? What combination of props will render <a> element? Imagine the confusion if this would have been a complex component.

As a user of this component, we won't know which properties to pass without looking at the implementation. That will result in a bad developer experience.

Discriminated Union

Luckily, we can improve prop types by using a discriminated union so that other developers know exactly how to use the component.

A discriminated union is a union data structure that holds various objects, with one of the objects identified directly by a discriminant. The discriminant is the first item to be serialized or deserialized

Discriminated Unions by IBM

We know that Button component should receive different props depending if we want to render <button> or <a> element. Therefore, we will have a different data structure for the button than for an anchor element.

In order to distinguish these two data structures, we will introduce a discriminant property called kind . This is a property that can be either "anchor" or "button" .

1type AnchorProps = {
2 kind: "anchor"
3 href: string
4 target: string
5}
6
7type ButtonProps = {
8 kind: "button"
9 onClick: VoidFunction
10}
11
12type Props = AnchorProps | ButtonProps
13
14const Button = (props: Props) => {
15 switch (props.kind) {
16 case "button":
17 return <button onClick={props.onClick}>Click Me!</button>
18 case "anchor":
19 return (
20 <a href={props.href} target={props.target}>
21 Click Me!
22 </a>
23 )
24 default:
25 throw new Error("Unexpected value.")
26 }
27}

We constructed component Props by using a discriminated union type of AnchorProps and ButtonProps .

1type Props = AnchorProps | ButtonProps

A switch case is used to narrow down the type. For anchor kind, Typescript will realize that the props object has additional two fields: href and target . Same for the button, after type narrowing, Typescript will know that onClick is inside the props object. The default case should never happen and Typescript will warn us about that.

Button component usage example
Proper types for Button component

Now other developers will know exactly how to use the component without looking at the implementation level.

Until next time 👋

🔖 Articles to check