Home > Software engineering >  Inferences from JSX vs calling React Components as functions
Inferences from JSX vs calling React Components as functions

Time:03-06

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 enter image description here

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.

  • Related