I have made a Button React Component:
function Button({ text, Icon, iconProps, ...buttonProps }: ButtonProps)
The props must have this structure:
- Only a
text
- Only an
Icon
- An
Icon
withiconProps
(theiconProps
can only be passed if theIcon
is also present) - A
text
, anIcon
- A
text
, anIcon
andiconProps
- It cannot be empty, it has to either have at least a
text
or anIcon
If I make all the props optional I loose the type safety I am aiming to. I’ve attempted to create an interface for each scenario and make a union of all of them, but it is not working and doesn’t seem like an elegant minimalistic solution:
interface ButtonOnlyText extends React.ButtonHTMLAttributes<HTMLButtonElement> {
text: string;
}
interface ButtonOnlyIcon extends React.ButtonHTMLAttributes<HTMLButtonElement> {
Icon: React.FunctionComponent<React.SVGProps<SVGSVGElement>>;
}
interface ButtonIconWithProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
Icon: React.FunctionComponent<React.SVGProps<SVGSVGElement>>;
iconProps: React.SVGProps<SVGSVGElement>;
}
interface ButtonTextAndIcon extends React.ButtonHTMLAttributes<HTMLButtonElement> {
text: string;
Icon: React.FunctionComponent<React.SVGProps<SVGSVGElement>>;
}
interface ButtonComplete extends React.ButtonHTMLAttributes<HTMLButtonElement> {
text: string;
Icon: React.FunctionComponent<React.SVGProps<SVGSVGElement>>;
iconProps: React.SVGProps<SVGSVGElement>;
}
type ButtonProps = ButtonOnlyText | ButtonOnlyIcon | ButtonIconWithProps | ButtonTextAndIcon | ButtonComplete
declare function Button({ text, Icon, iconProps, ...buttonProps }: ButtonProps): React.FC;
This is the link to Typescript playground with the code for seeing the error.
2
Answers
You can reduce your set of interfaces as
{ a: A; } | { a: A; b: B; }
is{ a: A; b?: B; }
:Then you can’t declare
Button
asSimply because there are interfaces in your disjunction that don’t have the props you’re trying to access. Typescript will tell you that these properties are not defined.
All you can do is declare it as
And then have some code that’d work like this:
And query for the props you want to access like this:
And handle all cases as you please.
You might want to expose these components instead:
That all delegate to the main
Button
.My experience trying to make this kind of complicated types is that it rarely ends well, it almost always ends up with types that are pretty difficult to use, so I would instead try to simplify things.
It seems like you have two different problems you want to solve:
For the first problem, I’m not sure how important it actually is. The users of your Button component will end up with a weird-looking empty button, which they will surely notice pretty quickly and fix by adding either a text or an icon (or both). So I would simply ignore this problem and make both optional.
For the second problem, instead of expecting a React component as the icon, you could accept instead some arbitrary JSX:
This way you simply need to accept a prop
icon?: ReactNode
and you don’t need to care abouticonProps
at all. It is up to the caller to pass the right JSX.You could even go one step further and simply accept
children: ReactNode
instead of bothtext
andicon
, which gives even more flexibility to the users of this component. Maybe someone wants to put the icon on the other side, or have two icons, or have a png icon, or have some bold text, etc.