Ashima
Ashima

Reputation: 4834

Typescript Type to accept a React component type with subset of Props

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

Answers (2)

Kunal Phaltankar
Kunal Phaltankar

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

adrisons
adrisons

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

Related Questions