How do I enforce passthrough props in TypeScript (in React) when passing a component as prop as well?

Viewed 470

Let's say I have 3 components (CompA, CompB, and CompC), and CompC invokes the CompB component, which invokes the CompA component:

type PropsCompA = { param1: string };
const CompA = (props: PropsCompA) => (
  <p>{props.param1}</p>
)
type PropsCompA2 = { param2: string };
const CompA2 = (props: PropsCompA2) => (
  <p>{props.param2}</p>
)

type PropsCompB = {
  comp: React.StatelessComponent<any>;
  subProps: Record<string, any>;
};
const CompB = (props: PropsCompB) => (
  <props.comp { ...props.subProps }/>
)

const CompC = () => (
  <>
    <CompB subProps={{ param1: 'hello' }} comp={CompA} />
    <CompB subProps={{ param2: 'hello' }} comp={CompA2} />
  </>
)

Is there a way to ensure that when invoking CompB, that the subProbs passed to that component align with what's expected for the properties for the comp property. I.e, right now CompA expects props as {param1: string}, and CompA2 expects props as {param2: string}. However, when changing the code to this:

const CompC = () => (
  <>
    <CompB subProps={{ param33: 'hello' }} comp={CompA} />
    <CompB subProps={{ param44: 'hello' }} comp={CompA2} />
  </>
)

TypeScript won't throw an error. Is there a way make it so TypeScript would throw an error? I assume it has something to do with this line:

type PropsCompB = {
  comp: React.StatelessComponent<any>;
  subProps: Record<string, any>;
};

But I'm not quite sure. I'm fairly familiar with Generics, and have been playing around with this using Generics for a while, but still haven't quite been able to get it to work.

2 Answers

You can make CompB and PropsCompB generic in the passed in sub-component props:

type PropsCompB<T> = {
    comp: React.StatelessComponent<T>;
    subProps: T;
};

const CompB = <T,>(props: PropsCompB<T>) => <props.comp {...props.subProps} />
const CompC = () => (
    <>
        <CompB subProps={{ param1: 'hello' }} comp={CompA} /> // ✅
        <CompB subProps={{ param2: 'hello' }} comp={CompA2} /> // ✅
        <CompB subProps={{ param33: 'hello' }} comp={CompA} /> // ❌
        <CompB subProps={{ param44: 'hello' }} comp={CompA2} /> // ❌ 
    </>
)

Playground sample

So the Record will take any string value as a key in your example. Source. To change this, you can type the key rather than allow it to be any string.

What you can do is something like this:

type ParamKeys = "param1" | "param2";
type PropsCompB = {
  comp: React.StatelessComponent<any>;
  subProps: Record<ParamKeys, any>;
};
Related