Reputation: 4834
There is a component Button with following props:
ButtonProps = {
variant: 'primary' | 'secondary' | 'tertiary';
label: string;
// a few more props like onChange, size etc.
}
Now, I want to create another component called "ButtonGroup" component that accepts a Button instance as a prop but can only accept primary or secondary variant. How can I enforce that?
ButtonGroup component looks this:
<ButtonGroup
primaryButton={<Button variant="primary">Submit</Button>}
otherButton={<Button variant="secondary">Cancel</Button>}
/>
Now, the props for ButtonGroup are as follwos:
type PrimaryButtonProps = Omit<ButtonProps, 'variant'> & {
variant: 'primary' | 'secondary';
};
type ButtonGroupProps = BaseComponentProps<'div'> & {
size?: 'small' | 'medium';
primaryButton: React.ReactElement<PrimaryButtonProps, typeof Button>;
otherButton?: React.ReactElement<OtherButtonProps, typeof Button>;
};
I expect primaryButton to be a Button instance will all Button props but restricting variant to be either primary or secondary. But, with this current implementation, typescript doesn't complain if I provide a tertiary variant too.
<ButtonGroup
primaryButton={<Button variant="tertiary">Submit</Button>} // TS SHOULD COMPLAIN BUT IT DOES NOT
/>
Upvotes: 5
Views: 4956
Reputation: 79
You can use PrimaryButton component instead of Button and it will highlight the variant other than primary or secondary.
Checkout the working CodeSandbox here.
interface ButtonProps {
variant: "primary" | "secondary" | "tertiary";
label: string;
}
function Button({ variant, label }: ButtonProps) {
return (
<button style={{ color: variant === "primary" ? "blue" : "red" }}>
{label}
</button>
);
}
type PrimaryButtonProps = Omit<ButtonProps, "variant"> & {
variant: "primary" | "secondary";
};
interface ButtonGroupProps {
primaryButton: React.ReactElement<PrimaryButtonProps>;
}
function ButtonGroup({ primaryButton }: ButtonGroupProps) {
return <div>{primaryButton}</div>;
}
// See below type assertion. It's a hack but it works :)
const PrimaryButton = Button as (props: PrimaryButtonProps) => JSX.Element;
export default function App() {
return (
<div className="App">
<ButtonGroup
primaryButton={<PrimaryButton variant="tertiary" label="Primary" />}
/>
</div>
);
}
Upvotes: 0
Reputation: 3723
In my opinion the cleanest solution would be to separate the implementation of each component to enforce its specific types.
interface ButtonProps {
variant: "primary" | "secondary" | "tertiary";
children?: React.ReactNode;
}
const Button = ({ variant, children }: ButtonProps): React.ReactElement => (
<button>{children}</button> // apply styles based on the variant
);
interface PrimaryButtonProps {
label: string;
variant: "primary" | "secondary";
}
const PrimaryButton = ({ label, variant }: PrimaryButtonProps) => (
<Button variant={{ variant }}>{{ label }}</Button>
);
So, when you create a ButtonGroup, you should pass the specific PrimaryButton type, instead the generic one
type ButtonGroupProps = BaseComponentProps<'div'> & {
size?: 'small' | 'medium';
primaryButton: React.ReactElement<PrimaryButtonProps, typeof PrimaryButton>;
// ...
};
<ButtonGroup
primaryButton={<PrimaryButton variant="tertiary">Submit</PrimaryButton>} // TS should complain here
/>
Hope this helps!
Upvotes: 4