Skip to content

Instantly share code, notes, and snippets.

@moritzsalla
Created May 22, 2025 15:48
Show Gist options
  • Save moritzsalla/b11947e83afb5cb06c35bf3cba3ef67e to your computer and use it in GitHub Desktop.
Save moritzsalla/b11947e83afb5cb06c35bf3cba3ef67e to your computer and use it in GitHub Desktop.
Creates a component that renders different variants based on a `type` prop.
type ComponentMap<Type extends Record<string, unknown>> = {
[Key in keyof Type]: React.FC<Type[Key]>;
};
type VariantProps<Type extends Record<string, unknown>> = {
[Key in keyof Type]: { type: Key } & Type[Key];
}[keyof Type];
type CreateVariantOptions<Type extends Record<string, unknown>> = {
render?: (
element: React.ReactElement,
props: VariantProps<Type>,
) => React.ReactElement;
};
/**
* Extracts the props type from any React component.
*
* @example
* ```tsx
* const MyComponent = createVariantComponent({...});
* type MyComponentProps = InferProps<typeof MyComponent>;
*
* // Use the extracted type
* const wrapper = (props: MyComponentProps) => <MyComponent {...props} />;
* ```
*/
// Using any as a constraint here, not as a type.
// Unknown is not generic enough.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type ExtractProps<Type extends React.ComponentType<any>> = Pretty<
React.ComponentProps<Type>
>;
/**
* Creates a component that renders different variants based on a `type` prop.
*
* @example
* ```tsx
* const Card = createVariantComponent({
* basic: BasicCard, // renders <BasicCard {...props} />
* premium: PremiumCard, // renders <PremiumCard {...props} />
* });
*
* <Card type="basic" title="Hello" />
* <Card type="premium" title="Hello" features={['a', 'b']} />
* ```
*
* @example
*
* If you need to add a wrapper around the component, you can use the `render` option:
* ```tsx
* const Card = createVariantComponent({...}, {
* render: (element, props) => {
* return <div className="wrapper">{element}</div>;
* },
* });
*/
export const createVariantComponent =
<
Type extends Record<
string,
// Dito. 👆
// eslint-disable-next-line @typescript-eslint/no-explicit-any
any
>,
>(
variants: ComponentMap<Type>,
options?: CreateVariantOptions<Type>,
) =>
(props: VariantProps<Type>) => {
const El = variants[props.type] as React.FC<Type[keyof Type]>;
const element = <El {...(props as Type[keyof Type])} />;
return options?.render ? options.render(element, props) : element;
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment