reactjs - Hur man skriver en Button-komponent som kan returnera <a> eller <button> och skicka vidare props


Försöker bygga en komponent som antingen kommer att returnera ett eller ett baserat på prop "as". Jag vill också sprida props från till vilken tagg som helst som renderas för att ge flexibilitet.

Problem Jag verkar inte kunna få typerna rätt så att elementet som returneras inte ger mig ett fel. Här är mitt exempel (förenklat):

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>;

};

Det fungerar bra om jag tar bort unionstypen och använder antingen AnchorProps eller ButtonProps och sedan kommenterar ut respektive if-/return-sats. Men så fort jag gör det till en union med AnchorProps | ButtonProps får jag fel på HTML- och -taggarna som säger att typerna inte matchar.

Jag känner att jag är väldigt nära men kan inte riktigt nå dit.

Svar #1

Det är svårt för Typescript att veta att props är kopplade om du delar upp props, och sedan begränsar en prop.

Istället, begränsa själva props-objektet, och dela sedan upp det.

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

Också children kan spridas med ...restProps. Du behöver inget speciellt fall för att hantera det om du inte vill avlyssna det av någon anledning.

Se lekplats

Svar #2

Detta skulle vara det bästa tillvägagångssättet. Du kommer att få det bästa typsstödet med komponenten eftersom det avgör vad ...rest-egenskaperna bör vara baserat på den as-egenskap du anger.

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