Reputation: 143
Is it possible to infer correct types for props from an unknown component passed also as a prop?
In case of known component (that exists in current file) I can get props:
type ButtonProps = React.ComponentProps<typeof Button>;
But if I want to create a generic component Box
that accepts a component in as
prop and the component's props in props
prop. The component can add some default props, have some behavior, it doesn't matter. Basically its similar to higher-order components, but its dynamic.
import React from "react";
export interface BoxProps<TComponent> {
as?: TComponent;
props?: SomehowInfer<TComponent>; // is it possible?
}
export function Box({ as: Component, props }: BoxProps) {
// Note: it doesn't have to be typed within the Box (I can pass anything, I can control it)
return <Component className="box" title="This is Box!" {...props} />;
}
function MyButton(props: {onClick: () => void}) {
return <button className="my-button" {...props} />;
}
// usage:
function Example() {
// I want here the props to be typed based on what I pass to as. Without using typeof or explicitly passing the generic type.
return (
<div>
<Box
as={MyButton}
props={{
onClick: () => {
console.log("clicked");
}
}}
>
Click me.
</Box>
</div>
);
}
requirements:
React.ComponentType<Props>
)Upvotes: 4
Views: 3868
Reputation: 249506
You can use the predefined react type ComponentProps
to extract the prop types from a component type.
import React from "react";
export type BoxProps<TComponent extends React.ComponentType<any>> = {
as: TComponent;
props: React.ComponentProps<TComponent>;
}
export function Box<TComponent extends React.ComponentType<any>>({ as: Component, props }: BoxProps<TComponent>) {
return <div className="box" title="This is Box!">
<Component {...props} />;
</div>
}
function MyButton(props: {onClick: () => void}) {
return <button className="my-button" {...props} />;
}
// usage:
function Example() {
// I want here the props to be typed based on what I pass to as. Without using typeof or explicitly passing the generic type.
return (
<div>
<Box
as={MyButton}
props={{ onClick: () => { } }}
></Box>
</div>
);
}
Depending on you exact use case the solution might vary, but the basic idea is similar. You could for example turn the type around a little bit and take in the props as the type parameter for the BoxProps
. That way you can constrain the component props to have some specific properties you can supply inside the Box
component:
export type BoxProps<TProps extends {title: string}> = {
as: React.ComponentType<TProps>;
} & {
props: Omit<TProps, 'title'>;
}
export function Box<TProps extends {title: string}>({ as: Component, props }: BoxProps<TProps>) {
return <div className="box" title="This is Box!">
<Component title="Title from box" {...props as TProps} />;
</div>
}
If you want to take in intrinsic tags, you can also add keyof JSX.IntrinsicElements
to the TComponent
constraint:
export type BoxProps<TComponent extends React.ComponentType<any> | keyof JSX.IntrinsicElements> = {
as: TComponent;
props: React.ComponentProps<TComponent>;
}
export function Box<TComponent extends React.ComponentType<any>| keyof JSX.IntrinsicElements>({ as: Component, props }: BoxProps<TComponent>) {
return <div className="box" title="This is Box!">
<Component {...props} />;
</div>
}
Upvotes: 5