Sure, I have several UI libraries published to a private registry on azure for my org (using TurboRepos). This is a ButtonAnchor Hybrid Component used in multiple codebases that truly segregates ButtonHTMLAttributes from AnchorHTMLAttributes -- but you can extend the logic with additional unions etc to achieve the desired polymorphic component outcome.
There is one definition I'll include before the code from the file for clarity
export namespace UI {
export module Helpers {
export type Without<T, U> = { [P in Exclude<keyof T, keyof U>]?: never };
/* Provides a truly mutually exclusive type union */
export type XOR<T, U> = T | U extends object
? (Without<T, U> & U) | (Without<U, T> & T)
: T | U;
}
}
However, for simplicity you can pull those two types out of the Namespace.module.[Type] chaining used in this ui component repo.
export type Without<T, U> = { [P in Exclude<keyof T, keyof U>]?: never };
/* Provides a truly mutually exclusive type union */
export type XOR<T, U> = T | U extends object
? (Without<T, U> & U) | (Without<U, T> & T)
: T | U;
Here is the file:
import type { FC, ButtonHTMLAttributes, AnchorHTMLAttributes, JSXElementConstructor } from "react";
import cn from "clsx";
import LoadingDots from "../LoadingDots";
import UI from "../../typedefs/namespace";
export type ButtonAnchorXOR = UI.Helpers.XOR<"a", "button">;
/**
* component types allowed by the (Button | Anchor) IntrinsicElements
*/
export type ButtonAnchorComponentType =
| "button"
| "a"
| JSXElementConstructor<
React.DetailedHTMLProps<
ButtonHTMLAttributes<HTMLButtonElement>,
HTMLButtonElement
>
> | JSXElementConstructor<
React.DetailedHTMLProps<
AnchorHTMLAttributes<HTMLAnchorElement>,
HTMLAnchorElement
>
>;;
/**
* Base props of the (Button | Anchor) components.
*/
export interface ButtonAnchorProps<
C extends ButtonAnchorComponentType = ButtonAnchorXOR
> {
href?: string;
className?: string;
variant?: "primary" | "secondary" | "ghost" | "violet" | "black" | "white";
size?: "sm" | "md" | "lg";
active?: boolean;
Component?: C;
width?: string | number;
loading?: boolean;
}
/**
* The HTML props allowed by the (Button | Anchor) components.
* These props depend on the used component type (C = "a" | "button").
*/
export type ButtonAnchorHTMLType<
C extends ButtonAnchorComponentType = ButtonAnchorXOR
> = C extends "a"
? AnchorHTMLAttributes<HTMLAnchorElement>
: ButtonHTMLAttributes<HTMLButtonElement>;
export type ButtonAnchorFC<
C extends ButtonAnchorComponentType = ButtonAnchorXOR
> = FC<ButtonAnchorHTMLType<C> & ButtonAnchorProps<C>>;
export type ButtonType = <C extends ButtonAnchorComponentType = "button">(
...args: Parameters<ButtonAnchorFC<C>>
) => ReturnType<ButtonAnchorFC<C>>;
export type AnchorType = <C extends ButtonAnchorComponentType = "a">(
...args: Parameters<ButtonAnchorFC<C>>
) => ReturnType<ButtonAnchorFC<C>>;
export type ButtonAnchorConditional<
T extends ButtonAnchorXOR = ButtonAnchorXOR
> = T extends "a"
? AnchorType
: T extends "button"
? ButtonType
: UI.Helpers.XOR<ButtonType, AnchorType>;
const Button: ButtonAnchorFC<"button"> = props => {
const {
width,
active,
children,
variant = "primary",
Component = "button",
loading = false,
style = {},
disabled,
size = "md",
className,
...rest
} = props;
const variants = {
primary:
"text-background bg-success border-success-dark hover:bg-success/90 shadow-[0_5px_10px_rgb(0,68,255,0.12)]",
ghost: "text-success hover:bg-[rgba(0,68,255,0.06)]",
secondary:
"text-accents-5 bg-background border-accents-2 hover:border-foreground hover:text-foreground",
black:
"bg-foreground text-background border-foreground hover:bg-background hover:text-foreground",
white: "bg-background text-foreground border-background hover:bg-accents-1",
violet: "text-background bg-violet border-violet-dark hover:bg-[#7123be]"
};
const sizes = {
sm: "h-8 leading-3 text-sm px-1.5 py-3",
md: "h-10 leading-10 text-[15px]",
lg: "h-12 leading-12 text-[17px]"
};
const rootClassName = cn(
"relative inline-flex items-center justify-center cursor pointer no-underline px-3.5 rounded-md",
"font-medium outline-0 select-none align-middle whitespace-nowrap",
"transition-colors ease-in duration-200",
variant !== "ghost" && "border border-solid",
variants[variant],
sizes[size],
{ "cursor-not-allowed": loading },
className
);
return (
<Component
aria-pressed={active}
data-variant={variant}
className={rootClassName}
disabled={disabled}
style={{
width,
...style
}}
{...rest}>
{loading ? (
<i className='m-0 flex'>
<LoadingDots />
</i>
) : (
children
)}
</Component>
);
};
const Anchor: ButtonAnchorFC<"a"> = props => {
const {
width,
active,
children,
variant = "primary",
Component = "a",
loading = false,
style = {},
size = "md",
className,
...rest
} = props;
const variants = {
primary:
"text-background bg-success border-success-dark hover:bg-success/90 shadow-[0_5px_10px_rgb(0,68,255,0.12)]",
ghost: "text-success hover:bg-[rgba(0,68,255,0.06)]",
secondary:
"text-accents-5 bg-background border-accents-2 hover:border-foreground hover:text-foreground",
black:
"bg-foreground text-background border-foreground hover:bg-background hover:text-foreground",
white: "bg-background text-foreground border-background hover:bg-accents-1",
violet: "text-background bg-violet border-violet-dark hover:bg-[#7123be]"
};
const sizes = {
sm: "h-8 leading-3 text-sm px-1.5 py-3",
md: "h-10 leading-10 text-[15px]",
lg: "h-12 leading-12 text-[17px]"
};
const rootClassName = cn(
"relative inline-flex items-center justify-center cursor pointer no-underline px-3.5 rounded-md",
"font-medium outline-0 select-none align-middle whitespace-nowrap",
"transition-colors ease-in duration-200",
variant !== "ghost" && "border border-solid",
variants[variant],
sizes[size],
{ "cursor-not-allowed": loading },
className
);
return (
<Component
aria-pressed={active}
data-variant={variant}
className={rootClassName}
style={{
width,
...style
}}
{...rest}>
{loading ? (
<i className='m-0 flex'>
<LoadingDots />
</i>
) : (
children
)}
</Component>
);
};
const PolyMorphicComponent = <T extends ButtonAnchorXOR = ButtonAnchorXOR>({
props,
type
}: {
props?: ButtonAnchorConditional<T>
type: T;
}) => {
switch (type) {
case "a":
return <Anchor Component="a" {...props as AnchorType} />;
case "button":
return <Button Component='button' {...props as ButtonType} />;
default:
return <>{`The type property must be set to either "a" or "button"`}</>;
}
};
Anchor.displayName = "Anchor";
Button.displayName = "Button";
export default PolyMorphicComponent;