Is it possible to infer the props of the first child element and enforce them in TypeScript? I can get it close, but it begins to fail with generics, and I haven’t been able to infer the type.
I’m trying to pass component props from a wrapper to the first child component with type-safety. When the prop doesn’t exist on the object, TS should fail, otherwise pass.
import React, {
Children,
isValidElement,
cloneElement,
ReactNode,
} from 'react';
interface WrapperProps<C> {
children: ReactNode;
// how can I make typeof Button generic/inferred from the code?
// firstChildProps: Partial<React.ComponentProps<typeof Button>>; // works with <C> removed
firstChildProps: Partial<React.ComponentPropsWithoutRef<C>>;
}
const Wrapper: React.FC<WrapperProps<typeof Button>> = ({
children,
firstChildProps,
}) => {
const firstChild = Children.toArray(children)[0];
if (isValidElement(firstChild)) {
// Clone the first child element and pass the firstChildProps to it
return cloneElement(firstChild, { ...firstChildProps });
} else {
return <>{children}</>;
}
};
interface ButtonProps {
disabled: boolean;
children: ReactNode;
}
const Button: React.FC<ButtonProps> = ({ disabled, children }) => {
return <button disabled={disabled}>{children}</button>;
};
const Example = () => {
return (
<>
{/* Passes type check because disabled exists on Button */}
<Wrapper firstChildProps={{ disabled: false }}>
<Button disabled={true}>Ok</Button>
</Wrapper>
{/* Fails type check because cheese does not exist on Button */}
<Wrapper firstChildProps={{ cheese: true }}>
<Button disabled={true}>Ok</Button>
</Wrapper>
</>
);
};
Here’s a nearly working TS Playground.
2
Answers
You just need to add generic constraint to
WrapperProps<C>
– the same, thatComponentPropsWithoutRef<C>
has – which isElementType
.C
generic type parameter, so that it suitsComponentProps
utility type, e.g. withElementType
, as proposed in SlavaSobolev’s answer:Wrapper
component generic as well, but you would have to dropReact.FC
which does not work with further generics:Then you can use it for different content:
Playground Link