Reputation: 65
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
Upvotes: 3
Views: 6526
Reputation: 110
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. :)
Upvotes: 0
Reputation: 187004
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; }'.
}
}
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())
Upvotes: 2