Polymorphic
Utility types for strongly typed polymorphic components.
Types for components that render a different element depending on an as prop. CoreUI uses them for icon slots, where asChild would mean cloning an element just to swap a tag:
<FancyButton.Icon as={RiArrowRightLine} />The types give as autocomplete and then type the remaining props against whatever component you passed. They're only needed if you use TypeScript.
Installation
Create a utils/polymorphic.ts file and paste the following code into it.
type AsProp<T extends React.ElementType> = { as?: T;};type PropsToOmit<T extends React.ElementType, P> = keyof (AsProp<T> & P);type PolymorphicComponentProp< T extends React.ElementType, Props = {},> = React.PropsWithChildren<Props & AsProp<T>> & Omit<React.ComponentPropsWithoutRef<T>, PropsToOmit<T, Props>>;export type PolymorphicRef<T extends React.ElementType> = React.ComponentPropsWithRef<T>['ref'];type PolymorphicComponentPropWithRef< T extends React.ElementType, Props = {},> = PolymorphicComponentProp<T, Props> & { ref?: PolymorphicRef<T> };export type PolymorphicComponentPropsWithRef< T extends React.ElementType, P = {},> = PolymorphicComponentPropWithRef<T, P>;export type PolymorphicComponentProps< T extends React.ElementType, P = {},> = PolymorphicComponentProp<T, P>;export type PolymorphicComponent<P> = { <T extends React.ElementType>( props: PolymorphicComponentPropsWithRef<T, P>, ): React.ReactNode;};Examples
Without forwardRef
PolymorphicComponentProps<T, OwnProps> merges your own props with the props of T, minus the ones you've already claimed. This is how every CoreUI icon slot is written — here is the real one from fancy-button.tsx:
import { PolymorphicComponentProps } from '@/utils/polymorphic';
function FancyButtonIcon<T extends React.ElementType>({
className,
variant,
size,
as,
...rest
}: PolymorphicComponentProps<T, FancyButtonSharedProps>) {
const Component = as || 'div';
const { icon } = fancyButtonVariants({ variant, size });
return <Component className={icon({ class: className })} {...rest} />;
}Because as defaults to div, both of these type-check, and the second one accepts every prop a Remix icon takes:
<FancyButton.Icon />
<FancyButton.Icon as={RiArrowRightLine} />With forwardRef
No CoreUI component needs this, since ref is a plain prop in React 19. If you're supporting an older major, the generic has to be threaded through by hand:
import * as React from 'react';
import { cn } from '@/utils/cn';
import {
PolymorphicComponent,
PolymorphicComponentPropsWithRef,
PolymorphicRef,
} from '@/utils/polymorphic';
type ButtonOwnProps = {
size?: 'large' | 'medium';
};
const Button: PolymorphicComponent<ButtonOwnProps> = React.forwardRef(
<T extends React.ElementType = 'button'>(
{
as,
className,
size = 'large',
...rest
}: PolymorphicComponentPropsWithRef<T, ButtonOwnProps>,
forwardedRef?: PolymorphicRef<T>,
) => {
const Component = as || 'button';
return (
<Component
ref={forwardedRef}
className={cn(
{
'h-12 px-4': size === 'large',
'h-9 px-3': size === 'medium',
},
className,
)}
{...rest}
/>
);
},
);API Reference
| Type | Use |
|---|---|
PolymorphicComponentProps<T, P> | Props for a polymorphic component without a ref |
PolymorphicComponentPropsWithRef<T, P> | The same, plus a correctly typed ref |
PolymorphicRef<T> | The ref type of T, for a forwardRef parameter |
PolymorphicComponent<P> | Annotates the component itself so the generic survives forwardRef |