diff --git a/src/index.ts b/src/index.ts index 0338099..8751b21 100644 --- a/src/index.ts +++ b/src/index.ts @@ -38,6 +38,7 @@ export * from './lifecycle/hooks/useIsMountedState/useIsMountedState.js'; export * from './lifecycle/hooks/useMount/useMount.js'; export * from './lifecycle/hooks/useUnmount/useUnmount.js'; export * from './nextjs/useIsomorphicLayoutEffect/useIsomorphicLayoutEffect.js'; +export * from './types/PolymorphicComponentProps/PolymorphicComponentProps.js'; export * from './utils/adjustFontSize/adjustFontSize.js'; export * from './utils/arrayRef/arrayRef.js'; export * from './utils/createTimeout/createTimeout.js'; diff --git a/src/types/PolymorphicComponentProps/PolymorphicComponentProps.mdx b/src/types/PolymorphicComponentProps/PolymorphicComponentProps.mdx new file mode 100644 index 0000000..d2a0e99 --- /dev/null +++ b/src/types/PolymorphicComponentProps/PolymorphicComponentProps.mdx @@ -0,0 +1,85 @@ +import { Meta } from '@storybook/blocks'; + + + +# PolymorphicComponentProps + +A collection of generic types to help build strongly typed Polymorphic Components, consistently. + +`PolymorphicComponentProps` takes care of combining your Component's unique prop types with those of +any HTML element or React Component you happen to be assigning to your Polymorphic Component. As an +extra precaution, a check is performed to omit any "default" prop type from the chosen element that +might conflict with one of the same name in your Component. + +## Usage + +```tsx +type FooComponentProps = PolymorphicComponentProps< + T, + { barProp: string | undefined } +>; + +type FooComponent = (props: FooComponentProps) => ReactNode | null; + +export const Foo: FooComponent = ({ + as: Component = 'div', + children, + ...otherProps +}: FooComponentProps) => { + return {children}; +}; +``` + +The type `FooComponentProps` consists of `T` and any custom prop types you require for +`` (in this example, `barProp`). The types for `T` are derived from the element in +the `as` prop: + +```tsx + + This is a proper link + + + + Foo as a heading: anything goes. + +``` + +So while two examples use the same FooComponent, their `FooComponent` types are not the same. They +are derived from the types from their respective `` and `

` elements. + +```tsx + + This button will throw an error + + + console.log("Clicked")}> + This button is okay + +``` + +### Using Refs + +A Polymorphic Component with Refs can be typed with `PolymorphicComponentPropsWithRef` and +`PolymorphicRef`: + +```tsx +type FooComponentProps = PolymorphicComponentPropsWithRef< + T, + { barProp: string | undefined } +>; + +type FooComponent = (props: FooComponentProps) => ReactNode | null; + +export const Foo: FooComponent = forwardRef( + ( + { as: Component = 'div', children, ...otherProps }: FooComponentProps, + ref?: PolymorphicRef, + ) => { + return ( + + {children} + + ); + }, +); +``` diff --git a/src/types/PolymorphicComponentProps/PolymorphicComponentProps.ts b/src/types/PolymorphicComponentProps/PolymorphicComponentProps.ts new file mode 100644 index 0000000..f4126b0 --- /dev/null +++ b/src/types/PolymorphicComponentProps/PolymorphicComponentProps.ts @@ -0,0 +1,31 @@ +import type { + ComponentPropsWithoutRef, + ComponentPropsWithRef, + ElementType, + PropsWithChildren, +} from 'react'; + +/** + * PropsFromAs is a type based on the supplied as prop. + */ +type PropsFromAs = { as?: Type }; + +/** + * MergeAndOverride is a type that omits type keys from BaseTypes that are also present in OverrideProps. + */ +type MergeAndOverride = Omit & + OverrideProps; + +export type PolymorphicRef = ComponentPropsWithRef['ref']; + +export type PolymorphicComponentProps< + BaseType extends ElementType, + OwnProps = Record, +> = PropsWithChildren< + MergeAndOverride, PropsFromAs & OwnProps> +>; + +export type PolymorphicComponentPropsWithRef< + BaseType extends ElementType, + OwnProps = Record, +> = PolymorphicComponentProps & { ref?: PolymorphicRef };