V1.0

Polymorphic

Utility types for creating strongly typed polymorphic components.

An alternative to asChild prop. Usually used for Icon components. It is preferred over asChild to shorten the code and avoid unnecessary cloning.

Provides autocomplete and type checking for the given component. It's only necessary if you're using TypeScript.

Installation

Create a utils/polymorphic.ts file and paste the following code into it.

/utils/polymorphic.ts
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

In this example, we're using Ri24HoursFill icon by default for <Icon> if as prop is not specified.

/demo-polymorphic-without-forwardref.tsx
import { PolymorphicComponentProps } from '@/utils/polymorphic';
import { cn } from '@/utils/cn';
import { type RemixiconComponentType, Ri24HoursFill } from '@remixicon/react';
 
function Icon<T extends RemixiconComponentType>({
  as,
  className,
  ...rest
}: PolymorphicComponentProps<T, {}>) {
  const Component = as || Ri24HoursFill;
 
  return <Component className={cn('size-4', className)} {...rest} />;
}
 
// How to use:
function Example() {
  return (
    <>
      <Icon />
      <Icon as={RiInformationLine} />
    </>
  );
}

With forwardRef

Never used with forwardRef in AlignUI, but here's how to use it if needed.

/demo-polymorphic-with-forwardref.tsx
import * as React from 'react';
 
import {
  PolymorphicComponent,
  PolymorphicComponentPropsWithRef,
  PolymorphicRef,
} from '@/utils/polymorphic';
import { cn } from '@/utils/cn';
 
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}
      />
    );
  },
);
© 2024 AlignUI Design System. All rights reserved.