reactjs - Cómo escribir un componente Button que pueda devolver <a> o <button> y pasar propiedades
Intentando construir un componente que devolverá ya sea un o un basado en la propiedad "as". También quiero expandir las propiedades de a cualquier etiqueta que se renderice para dar flexibilidad.
Problema No puedo lograr que los tipos sean correctos para que el elemento que se devuelve no me dé un error. Aquí está mi ejemplo (simplificado):
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>;
};
Funciona bien si elimino el tipo unión y uso ya sea AnchorProps o ButtonProps y luego comento la declaración if/return respectiva. Pero en cuanto lo hago una unión con AnchorProps | ButtonProps me da errores en las etiquetas HTML y diciendo que los tipos no coinciden.
Siento que estoy muy cerca pero no puedo llegar del todo.
Respuesta #1
Es difícil para Typescript saber que las props están conectadas si separas las props y luego restringes una prop.
En su lugar, reduzca el objeto props en sí mismo, y luego sepárelo.
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
};
Además, los children pueden repartirse con el ...restProps. No necesitas un caso especial para manejar eso a menos que quieras interceptarlo por alguna razón.
Ver patio de recreo
Respuesta #2
Este sería el mejor enfoque. Obtendrás el mejor soporte de tipos con el componente, ya que determina cuáles deberían ser las propiedades ...rest en función de la propiedad as que le proporciones.
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>
);
}
