Home > Software engineering >  How to type a Button component that could return <a> or <button> and pass props
How to type a Button component that could return <a> or <button> and pass props

Time:09-29

Trying to build a component that will return either an or a based on the prop "as". I also want to spread the props from to whatever tag gets rendered to give flexibility.

Problem I can't seem to get the types correct so that the element that is being returned doesn't give me an error. Here is my example (simplified):

interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
  as: "button";
}

interface AnchorProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
  as: "link";
}

const Button = ({
  children,
  as,
  ...restProps
}: AnchorProps | ButtonProps) => {

  if (as === "link") return <a {...restProps}>{children}</a>;

  if (as === "button") return <button {...restProps}>{children}</button>;

};

It works fine if I remove the union type and use either AnchorProps or ButtonProps and then comment out the respective if/return statement. But as soon as I make it a union with AnchorProps | ButtonProps it gives me errors on the HTML and tags saying the types don't match.

I feel like I'm real close but can't quite get there.

CodePudding user response:

It's hard to Typescript to know that props are connected if you break the props apart, and then narrow one prop.

Instead, narrow the props object itself, then break it apart.

const Button = (props: (AnchorProps | ButtonProps)) => {
  if (props.as === "link") {
    const { as, ...restProps } = props
    return <a {...restProps}/>;
  }

  if (props.as === "button") {
    const { as, ...restProps } = props
    return <button {...restProps}/>;
  }

  return null
};

Also children can be spread with the ...restProps. You don't need a special case to handle that unless you want to intercept it for some reason.

See Playground

CodePudding user response:

This would be the best approach. You will get the best type support with the component since it determines what the ...rest props should be based on the as prop you provide it.

import React, { ReactNode } from 'react';

type AnchorProps = React.AnchorHTMLAttributes<HTMLAnchorElement>;
type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement>;

enum ButtonComponentType {
    LINK = 'link',
    BUTTON = 'button',
}

type ButtonComponentProps<T extends ButtonComponentType> = {
    as: T;
    children: ReactNode;
    // based on the "as" value, either intersect with ButtonProps or AnchorProps
} & (T extends ButtonComponentType.BUTTON ? ButtonProps : AnchorProps);

export function Button<T extends ButtonComponentType>({ children, as, ...restProps }: ButtonComponentProps<T>) {
    if (as === ButtonComponentType.LINK) return <a {...(restProps as AnchorProps)}>{children}</a>;
    if (as === ButtonComponentType.BUTTON) return <button {...(restProps as ButtonProps)}>{children}</button>;
    return null;
}

// neither of these throw an error
function Test1() {
    return (
        // doesn't have "href" because the "as" prop is set to "button"
        <Button as={ButtonComponentType.BUTTON} type="submit">
            <p>test</p>
        </Button>
    );
}

function Test2() {
    return (
        // doesn't have button props
        <Button as={ButtonComponentType.LINK} href="http://google.com">
            <p>test</p>
        </Button>
    );
}
  • Related