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.
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>
);
}