3
import IntroductionArea from "../components/IntroductionArea";

interface AppSection {
  url: string;
  component: React.FC<any> | React.LazyExoticComponent<React.FC<any>>;
  props?: React.ComponentProps<any>;
}

export const appSections: AppSection[] = [
  {
    url: "/home",
    component: IntroductionArea,
    props: {
      text: "Hello World",
      name: "Foo Bar"
    }
  },
];

Hello. I have code that creates an array of objects which accept a component and the desired props to that component. The below code is functional but is not type-safe because the props member of the AppSection interface accepts any. I tried replacing any with typeof component but you cannot reference component in the interface which it is declared.

How can I make this code type-safe while maintaining same functionality?

Thanks

1
  • Not an answer but why not just { url: "/home", content: <IntroductionArea text= 'HelloWorld' name="Foo Bar" /> }? Commented Aug 26, 2021 at 21:30

2 Answers 2

2

You need to generic types to do make one value's type depend on another values type.

In this case, you need to props type for a component. So let's start there:

type Component = React.FC<any> | React.LazyExoticComponent<React.FC<any>>

interface AppSection<C extends Component> {
  url: string;
  component: C;
  props?: React.ComponentProps<C>;
}

Now to test that:

const introSection: AppSection<typeof IntroductionArea> = {
    url: "/home",
    component: IntroductionArea,
    props: {
        text: "Hello World",
        name: "Foo Bar",
        noPropHere: false // Type '{ text: string; name: string; noPropHere: false; }' is not assignable to type '{ text: string; name: string; }'.
    }
}

Playground

Good, we get the error we except which proves that it's working.


Making an array of these is much harder. You can't just do AppSection[], because the generic parameter to AppSection is required. So you could do:

const introSection: AppSection<typeof IntroductionArea> = {
    url: "/home",
    component: IntroductionArea,
    props: {
        text: "Hello World",
        name: "Foo Bar",
        noPropHere: false // error as expected
    }
}

const otherSection: AppSection<typeof OtherArea> = {
    url: "/home",
    component: OtherArea,
    props: {
        other: "Hello World",
    }
}

export const appSections = [introSection, otherSection]

But then you get a type error if you try to render these:

const renderedSections = appSections.map(item => {
    const SectionComponent = item.component
    return <SectionComponent {...item.props} />
    // Type '{} | { text: string; name: string; } | { other: string; }' is not assignable to type 'IntrinsicAttributes & { text: string; name: string; } & { other: string; }'.
    //  Type '{}' is missing the following properties from type '{ text: string; name: string; }': text, name(2322)
})

What that means is that typescript can't guarantee that the props of an item in the array actually match up with the component from that item in the array.


In closing, React Router is a good reference for how they handle this since the use case here is very similar. They allow you to declare what a route renders in two ways:

<Route path="/home"><Home /></Route>
<Route path="/other" render={() => <Other />} />

Note that in both cases you are rendering the component yourself and just passing the result. This way the component can just validate it own props and it doesn't need to worry about all those details. Which, as you can see from the above, makes things much easier.

In your case that might look something like:

interface AppSection {
  url: string;
  render(): React.ReactNode;
}

function IntroductionArea(props: {text: string, name: string}) {
    return null
}

function OtherArea(props: {other: string}) {
    return null
}

export const appSections: AppSection[] = [
  { url: '/home', render: () => <IntroductionArea text="Hello, World!" name="Foo Bar"/> },
  { url: '/other', render: () => <OtherArea other="Other text" /> },
];

// Render all sections
const renderedSections = appSections.map(section => section.render())

Playground

Sign up to request clarification or add additional context in comments.

1 Comment

This is great! I was able to get this working in my project but had to modify interface to render(): React.ReactElement and in the the array set the property as render: () => React.createElement(IntroductionLazy, {text: "Hello World", name: "Foo Bar"}). If I were to initialize as <IntroductionLazy/> I would get Cannot use namespace 'IntroductionArea' as a type error.
0

You could do this:

import IntroductionArea from "../components/IntroductionArea";

interface AppSectionProps {
  text: string;
  name: string;  
}

interface AppSection {
  url: string;
  component: React.FC<any> | React.LazyExoticComponent<React.FC<any>>;
  props?: React.ComponentProps<AppSectionProps>;
}

export const appSections: AppSection[] = [
  {
    url: "/home",
    component: IntroductionArea,
    props: {
      text: "Hello World",
      name: "Foo Bar"
    }
  },
];

Then in <IntroductionArea /> you would just do:

const IntroductionArea = (props: AppSectionProps) => {
  return <div></div>;
};

And you're good to go. :)

2 Comments

I think the point is to do this with many diffrerent components, each of which have different props, so I don't think this helps achieve that goal.
If that's the case then I'm not sure how easy it would be to get it to be type-safe unless you write out a whole lot of typings and just write props?: React.ComponentProps<interface1 || interface 2 || interface3>;

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.