Many times we have used the design system with React button
components or we have designed one ourselves that might look something similar to this:
const App= () => {
return (
<main>
<Button primary>Primary</Button>
<Button secondary>Secondary</Button>
<Button destructive>Destructive</Button>
<Button>Default</Button>
</main>
);
};
BTW react will take primary
, secondary
or destructive
props as truthy so it's the same as writing primary={true}
This looks good but it wouldn't be great if we allow such use case like this:
<Button primary secondary destructive>Primary</Button>
It doesn't make sense though but we can do this anyway and our styles might get overridden depending on Button
implementation details where the Button
styles get's generated. So how can we avoid this? Here we go with Typescript.
Let's take a look at our Button
component:
type ButtonProps = {
children: string;
destructive?: boolean;
secondary?: boolean;
primary?: boolean;
};
function Button({
children,
primary = false,
secondary = false,
destructive = false,
}: ButtonProps) => {
const style = React.useMemo(() => {
if (primary) {
return { background: 'BlueViolet', color: 'white' };
}
if (secondary) {
return { background: 'brown', color: 'white' };
}
if (destructive) {
return { background: 'red', color: 'white' };
}
return { background: 'white', color: 'black' };
}, [primary, secondary, destructive]);
return <button style={style}>{children}</button>;
};
Nothing too fancy here. We pass our props
to Button
component, styles get's generated based on those props and button
component is returned.
Based on ButtonProps
type, our Button
takes all the props as optional (?). This is not ideal. We most likely want to make it more strict so only one of these props (primary
, secondary
or destructive
) could be passed to the component. How can we do that? Create a union type.
type DefaultButtonProps = {
children: string;
};
type PrimaryButtonProps = DefaultButtonProps & {
primary: boolean;
secondary?: never;
destructive?: never;
};
type SecondaryButtonProps = DefaultButtonProps & {
secondary: boolean;
primary?: never;
destructive?: never;
};
type DestructiveButtonProps = DefaultButtonProps & {
destructive: boolean;
secondary?: never;
primary?: never;
};
type EmptyButtonProps = DefaultButtonProps & {
destructive?: never;
secondary?: never;
primary?: never;
};
type ButtonProps =
| PrimaryButtonProps
| SecondaryButtonProps
| DestructiveButtonProps
| EmptyButtonProps;
Here we have separated our types and made a union type (|). That means our button/TypeScript will only be happy if we pass only one of these: PrimaryButtonProps or SecondaryButtonProps or DestructiveButtonProps or EmptyButtonProps
. In every type that we have declared we say "this particular prop in this type should be type of never"
.
Now if we try to do something like this the TypeScript
will get angry:
<Button primary secondary destructive>Primary</Button>
![image.png]`(cdn.hashnode.com/res/hashnode/image/upload…)
This is great. If we will try to use our Button
component with a different intent than it is designed for - Typescript
will warn us . Although if you play with it you might notice another sadness. The Button component is not happy when we pass a totally valid prop to it like id
, name
, type
etc. 😱
Spoiler alert this will be covered in the next post. Be prepared for some more TypeScript awesomeness 🙌
This post was inspired by frontendmasters
course on React and TypeScript
. Highly recommend. Check it out