I intend to tell you how leaders communicate
August 13, 2023 - 5 min read
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?: string3 target?: string4 onClick?: VoidFunction5}67const 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 }1516 if (props.onClick) {17 return <button onClick={props.onClick}>Click Me!</button>18 }1920 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.
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 IBMWe 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: string4 target: string5}67type ButtonProps = {8 kind: "button"9 onClick: VoidFunction10}1112type Props = AnchorProps | ButtonProps1314const 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.
Now other developers will know exactly how to use the component without looking at the implementation level.
Until next time 👋