Search code examples
javascriptreactjstypescriptrecursion

Infer React child props in TypeScript interface


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.


Solution

    1. You need to constrain the 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>>;
    }
    
    1. You can make the 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>
    

    Playground Link