Skip to content

Instantly share code, notes, and snippets.

@lilBunnyRabbit
Last active December 5, 2025 10:46
Show Gist options
  • Select an option

  • Save lilBunnyRabbit/f410653edcacec1b12cb44af346caddb to your computer and use it in GitHub Desktop.

Select an option

Save lilBunnyRabbit/f410653edcacec1b12cb44af346caddb to your computer and use it in GitHub Desktop.
Polymorphic React Component
const PolyComponent = createPolymorphic<
{
download?: boolean;
className?: string;
children?: React.ReactNode;
},
{
value: number;
}
>((Component, { value, className, ...props }) => (
<Component className={`bg-red-500 text-blue-500 ${className}`} {...props}>
Value is {value}{props.download ? "(click to download)" : ""}
</Component>
));
const InvalidComponent = ({ foo }: { foo: string }) => foo;
const ValidComponent = ({
href,
...props
}: {
href: string;
download?: boolean;
className?: string;
children?: React.ReactNode;
}) => <a href={href} {...props} />;
export function Test() {
return (
<>
<PolyComponent as={ValidComponent} href="/my-file.pdf" value={123} />
<PolyComponent
as="a"
value={123}
// Correctly inferred as HTMLAnchorElement
onClick={(e: React.MouseEvent<HTMLAnchorElement, MouseEvent>) =>
console.log('clicked', e)
}
// You can also pass required properties to the component.
className="bg-blue-500"
/>
{/* Invalid components */}
<PolyComponent as={InvalidComponent} value={123} foo="123" />
{/* Type '({ foo }: { foo: string; }) => string' is not assignable to type 'never'. */}
<PolyComponent as="div" value={123} />
{/* Type 'string' is not assignable to type 'never'. */}
{/* Missing props components */}
<PolyComponent as={ValidComponent} value={123} />
{/* Property 'href' is missing in type {...} */}
<PolyComponent as={ValidComponent} bar="123" />
{/* Property 'bar' does not exist on type {...} */}
{/* Invalid props */}
<PolyComponent as={ValidComponent} value="123" bar={123} />
{/* Type 'string' is not assignable to type 'number'. */}
</>
);
}
// A way more expensive check. Not used in this implementation.
// type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends <T>() => T extends Y
// ? 1
// : 2
// ? true
// : false;
// type IsEqualProps<Source, Compare> = keyof Source extends keyof Compare
// ? Equal<
// Source,
// {
// [K in keyof Source as K extends keyof Compare ? K : never]: Compare[K];
// }
// >
// : false;
/**
* Helper type to allow for polymorphic components to be used with any element.
*/
type PolymorphicElement<T = any> =
| keyof React.JSX.IntrinsicElements
| React.JSXElementConstructor<T>;
/**
* This is a prop level check since as could either be a component or an element reference.
*
* @template TElementProps - The props of the target element/component.
* @template TProps - Props that must be compatible with the target element.
* @template TAdditional - Additional props used by the render function.
*/
type ValidPolymorphicProps<TElementProps, TProps, TAdditional> =
keyof TProps extends keyof TElementProps
? Partial<TProps> &
TAdditional &
Omit<TElementProps, keyof TProps | keyof TAdditional | 'as'>
: never;
/**
* We dont list only valid elements due to performance reasons. The element validation is done
* by checking if the required props are a subset of the element props.
*
* If the check fails we return `never` (due to performance reasons) so that it invalidates the
* whole component.
*
* @template TElement - The element type to render (intrinsic element string or component).
* @template TProps - Props that must be compatible with the target element.
* @template TAdditional - Additional props used by the polymorphic component.
*/
type PolymorphicProps<
TElement extends PolymorphicElement,
TProps,
TAdditional,
> = {
/**
* `never` will override this property if the element props are not valid.
*/
as: TElement;
} & ValidPolymorphicProps<React.ComponentProps<TElement>, TProps, TAdditional>;
/**
* Type representing a polymorphic component render function.
*
* @template TProps - Props that the render function will pass to the component.
* @template TAdditional - Additional props used by the render function.
*/
type PolymorphicRender<TProps, TAdditional> = <
TComponent extends PolymorphicElement,
>(
props: PolymorphicProps<TComponent, TProps, TAdditional>,
) => React.ReactNode | Promise<React.ReactNode>;
/**
* Creates a polymorphic component that can render as any valid React element or component.
*
* @template TProps - Props that the render function will pass to the component.
* @template TAdditional - Additional props used by the render function.
*/
export function createPolymorphic<
TProps,
TAdditional = Record<string, unknown>,
TRenderProps = Partial<TProps> & TAdditional & Record<string, unknown>,
>(
render: (
Component: PolymorphicElement<TProps>,
props: TRenderProps,
) => React.ReactElement,
): PolymorphicRender<TProps, TAdditional> {
return ({ as: Component, ...props }) => {
// It's more performant to just type cast here since we know the props are valid.
return render(Component, props as TRenderProps);
};
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment