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.
C
generic type parameter, so that it suits ComponentProps
utility type, e.g. with ElementType
, as proposed in SlavaSobolev's answer:interface WrapperProps<C extends React.ElementType> {
children: ReactNode;
firstChildProps: Partial<React.ComponentPropsWithoutRef<C>>;
}
Wrapper
component generic as well, but you would have to drop React.FC
which does not work with further generics:const Wrapper = <C extends React.ElementType>({ children, firstChildProps }: WrapperProps<C>) => {
// etc.
}
Then you can use it for different content:
interface OtherProps {
foo: "bar";
}
const Other: React.FC<OtherProps> = () => null;
{/* Fails type check because cheese does not exist on Button */}
<Wrapper<typeof Button> firstChildProps={{ cheese: true }}>
<Button disabled={true}>Ok</Button>
</Wrapper>
{/* Error: Type 'number' is not assignable to type '"bar"'. */}
<Wrapper<typeof Other> firstChildProps={{ foo: 0 }}>
<Other foo="bar" />
</Wrapper>