Sign in
Log inSign up
Typescript patterns for a component library (part 1)

Typescript patterns for a component library (part 1)

Karolis Stulgys's photo
Karolis Stulgys
·Oct 15, 2021·

3 min read

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