Make React prop depends to other props
I'll share here a trick I used recently to make the existence of a prop (react) value depend on another using TypeScript.
The context
We have a Menu
component that can have as children MenuItem
or a custom react component.
Let's say the signature of MenuItem
is like this :
// MenuItem.tsx
export type MenuItemProps = {
... // some props
label: string
}
export const MenuItem: React.FC<MenuItemProps> = ({ label }) => {
return (
<div>
<h1>{label}</h1>
</div>
)
}
And the Menu
component takes a list of MenuItem
props and displays each MenuItem
with its props:
// Menu.tsx
export type MenuProps = {
... // some props
menuItemList: MenuItemProps[]
}
export const Menu: React.FC<MenuProps> = ({menuItemList}) => {
return (
<div>
<h1>Menu</h1>
<div>
{
menuItemList.map((menuItem, index) => <MenuItem key={index} {...menuItem} />)
}
</div>
</div>
)
}
The problem
Now, I want that the user can display a custom component that he/she builds. Let's say a div with icon. We need children
as props to Menu
like this:
export type MenuProps = {
... // some props
menuItemList: MenuItemProps[]
children: React.ReactNode
}
But the problem: where do I put the children prop?
- Before or after the list of MenuItem ?
- Between the list of MenuItem ? (More pains for nothing)
Yes, because there will be some devs who will provide these two props even if there are optionals.
I finally decide: Only one of them will be available to use so "If user/dev set menuItemList array he/she cannot set children prop".
The solution: TypeScript + never + union
We can try to define these two optional props and then check their existence and raise an error if both are available. I did not like this and did not try to implement it. The solution I used has the same logic but cleaner.
If like me, you are wondering where we can use never, be patient, here you'll use it 🤣
The final type will be in two parts.
1st step
export type MenusProps = {
children?: null | undefined,
menuItemList?: MenuItemProps[],
}
We make all of them optionals. Since we want to base our condition to the availability of children
, we make it optional as well.
2nd step
export type MenusProps = {
children: React.ReactNode,
menuItemList?: never,
}
With this type, MenuProps
can have children
as prop but not menuItemList
.
3rd step
export type MenusProps =
| {
children?: null | undefined,
menuItemList?: MenuItemProps[],
}
| {
children: React.ReactNode,
menuItemList?: never,
}
We now indicate that MenuProps
is an union of the above types. So it can have one of them but not both.
In this case, if the user of Menu
component provides children
as prop, he/she cannot provides again menuItemList
as
prop and if he/she can pass nothing.
Bonus
The final type is
export type MenusProps =
| {
children?: null | undefined,
menuItemList?: MenuItemProps[],
}
| {
children: Exclude<React.ReactNode, null | undefined>,
menuItemList?: never,
}
A React.ReactNode
can be null | undefined
and so we have two optionals properties in the second union which is not the desired behavior. So we Exclude
some values for children in this case and so we can write
// Menu.tsx
export type MenusProps =
| {
children?: null | undefined;
menuItemList?: MenuItemProps[];
}
| {
children: Exclude<React.ReactNode, null | undefined>;
menuItemList?: never;
}
export const Menu: React.FC<MenuProps> = ({ menuItemList }) => {
return (
<div>
<h1>Menu</h1>
<div>
{menuItemList && {menuItemList.map((menuItem, index) => (
<MenuItem key={index} {...menuItem} />
))}}
{children}
</div>
</div>
)
}
Now, if you try to give a children and a menuItemList to Menu, TypeScript will display a type incompatibility warning.
Conclusion
This solution may not be the best but it helps us to make some props depends to another and TypeScript can raise a warning if user didn't respect this rule.
I share a few tips that I use in my daily tasks, and I hope you have others that you want to share with us. I'm open to tips, and my social accounts are there for that.
So can you smell what Tawaliou is cooking ?