kripod / react-polymorphic-types

Zero-runtime polymorphic component definitions for React
MIT License
195 stars 8 forks source link

Wrapping polymorphic component with pass through `as` #3

Closed rafgraph closed 3 years ago

rafgraph commented 3 years ago

Hi, great library, thanks for creating it!

I'm having an issue when wrapping a polymorphic component with pass through as, the resulting component ends up untyped.

Building off of your Heading example in the readme I have: For a live reproduction see: https://codesandbox.io/s/polymorphic-pass-through-as-mnpff?file=/src/App.tsx

interface AdditionalProps {
  additionalProp?: string;
}
type PassThroughAsWithAddedPropProps<
  T extends React.ElementType = typeof HeadingDefaultElement
> = AdditionalProps & Omit<HeadingProps<T>, keyof AdditionalProps>;

// this does not work when adding additional props
// the <PassThroughAsWithAddedProp> component ends up as not typed, can pass any prop to it
function PassThroughAsWithAddedProp<
  T extends React.ElementType = typeof HeadingDefaultElement
>({ additionalProp, ...props }: PassThroughAsWithAddedPropProps<T>) {
  return <Heading {...props} />;
}
kripod commented 3 years ago

Hello,

Thank you for opening this issue. Unfortunately, with TypeScript’s current limitations, you may not want to create a generic PassThroughAsWithAddedProp component to rely upon, but copy the usage patterns exactly for every component you want to be polymorphic.

I noticed that instead of PolymorphicPropsWithoutRef, you’re using AdditionalProps & Omit<HeadingProps<T>, keyof AdditionalProps>, from which the as prop may be missing. Please refer to the example inspired by the readme pasted below, or check out how I implement polymorphic button components nowadays.

import type { PolymorphicPropsWithoutRef } from "react-polymorphic-types";

// An HTML tag or a different React component can be rendered by default
export const BoxDefaultElement = "div";

// Component-specific props should be specified separately
export type BoxOwnProps = {
  color?: string;
};

// Extend own props with others inherited from the underlying element type
// Own props take precedence over the inherited ones
export type BoxProps<
  T extends React.ElementType = typeof BoxDefaultElement
> = PolymorphicPropsWithoutRef<BoxOwnProps, T>;

export function Box<
  T extends React.ElementType = typeof BoxDefaultElement
>({ as, color, style, ...restProps }: BoxProps<T>) {
  const Element: React.ElementType = as || BoxDefaultElement;
  return <Element style={{ color, ...style }} {...restProps} />;
}