I'm having a little trouble understanding how Typescript infers props in JSX. The issues has come up in the following context -- I'm trying to make a component that accepts another component as a prop, e.g. (a simplified example):
function WrapperComponent<T extends FooComponentType>(
{FooComponent}:{FooComponent:T}
){
return FooComponent({className:"my-added-class"})
}
Where FooComponentType
is the type of a component that has a className
prop:
type FooComponentType = (props:{className:string}) => JSX.Element
What I'd like, is for Typescript to disallow any attempt to pass in a component that doesn't have a className
prop. Now, I recognize the typing above will not achieve that because React components are functions and so
type FooComponentType = (props: { className: string }) => JSX.Element;
function wrapComponent<C extends FooComponentType extends C ? any : never>(
Wrapped: C
) {
return React.createElement(Wrapped, { className: "my-added-class" });
}
function ComponentNoClass(props: {}) {
const [state, setState] = React.useState(0);
return (
<div onClick={() => setState((x) => x 1)}>
my classname is... oops I don't have a classname. [{state}]
</div>
);
}
function ComponentWClass(props: { className: string }) {
const [state, setState] = React.useState(0);
return (
<div onClick={() => setState((x) => x 1)}>
my classname is {props.className}. [{state}]
</div>
);
}
function ComponentWClassPlus(props: { className: string; foo: number }) {
const [state, setState] = React.useState(0);
return (
<div onClick={() => setState((x) => x 1)}>
my classname is {props.className}. [{state}]
</div>
);
}
export default function App() {
const [state, setState] = React.useState(0);
const jsx1 = wrapComponent(ComponentNoClass); // not fine
const jsx2 = wrapComponent(ComponentWClass); // fine
const jsx3 = wrapComponent(ComponentWClassPlus); // fine
return (
<div>
{jsx1}
{jsx2}
{jsx3}
<button onClick={() => setState((x) => x 1)}>
increment outer count. [{state}]
</button>
</div>
);
}
What's going on here?
wrapComponent()
is basically a "higher order component" function, that returns some rendered JSX from some component function/class that you call it with.
return React.createElement(Wrapped, { className: "my-added-class" });
The special sauce that gives you the type checking you crave is this: C extends FooComponentType extends C ? any : never
.
It essentially says, "generic type C
extends the conditional type (does FooComponentType
extend the generic type C
? If so, this type is any
, otherwise it is never
)".
It's a sort of counterintuitive way to get around the covariant issue you mentioned earlier, using a conditional type never
to assert the type. It feels circular somehow (C extends FooComponentType extends C
???) but I guess because of the way TypeScript evaluates the conditional type from the inside out, it's fine?
You were pretty close with your implementation of CheckedWrapperComponent
, but this way you cut out that middle man and only need to use the higher order component function.