2

Trying to create a dynamic React component. What is the proper way to conditionally pass the correct propType based on the selected component. Here's what I have so far I'm getting an error on this line <SelectComponent {...props.props} /> because the props don't match the components:

export interface SidebarProps {
    type: keyof typeof components,
    props: AddEditJobsProps | AddEditCustomersProps
}

const components = {
  job: AddEditJobs,
  customer: AddEditCustomers
};

export default function Sidebar(props: SidebarProps) {
  const { open, toggle } = useToggle();
  const SelectComponent = components[props.type];

  return (
    <RightSidebar open={open} toggleDrawer={toggle}>
      <SelectComponent {...props.props} />
    </RightSidebar>
  );
}

Edit: adding any to the props fixes the error however Typescript won't be able to match the selected type with the corresponding props during type checking which is what I'm hoping to accomplish here.

export interface SidebarProps {
    type: keyof typeof components,
    props: AddEditJobsProps | AddEditCustomersProps | any
}
4
  • So SelectComponent could be either AddEditJobs or AddEditCustomers depending on the value of props.type? Commented Feb 8, 2020 at 0:12
  • What is your desired behavior? Are you just trying to get rid of the error? Or do you want Typescript to actually infer the type of props.props? The former might be possible, but the latter is impossible because the actual type of SelectComponent will be unknown until runtime and can not be inferred by Typescript. Commented Feb 8, 2020 at 0:22
  • @jered good questions, I don't care about runtime, my desired behavior is to get good feedback from typescript during coding, so when I pass the "type" to my dynamic sidebar component I get to see what "props" I need to pass as well. Commented Feb 8, 2020 at 0:42
  • Link to a similar problem with a nice solution, Commented Sep 27, 2022 at 6:08

2 Answers 2

4

If you insist on keeping it dynamic that way, here's how you should probably do it.

Problem number one, as I mentioned in my comment, is that TS does not know what the value of props.type will be until runtime, so it cannot effectively infer what it should be ahead of time. To solve this, you need something like a plain old conditional that will explicitly render the correct component:

export const Sidebar: React.FC<SidebarProps> = props => {
  const { open, toggle } = useToggle();

  let inner: React.ReactNode;
  if (props.type === "job") {
    inner = <AddEditJobs {...props.props} />;
  } else if (props.type === "customer") {
    inner = <AddEditCustomers {...props.props} />;
  }

  return (
    <RightSidebar open={open} toggleDrawer={toggle}>
      {inner}
    </RightSidebar>
  );
};

Note that the conditional basically asserts what the value of that field will be, which helps TS infer what the rest of the shape will be and improves code-time type checking.

Problem number two is that your interface for SidebarProps is too permissive.

You have this:

export interface SidebarProps {
    type: keyof typeof components,
    props: AddEditJobsProps | AddEditCustomersProps
}

This is basically telling Typescript "the object should have a type field matching one of the keys in components, and a props field that is either AddEditJobsProps or AddEditCustomersProps". But it's not specifying that if type equals "job" then the props field must match AddEditJobsProps. To do that you need to more explicitly say so:

export type SidebarProps =
  | {
      type: "job";
      props: AddEditJobsProps;
    }
  | { type: "customer"; props: AddEditCustomersProps };

This uses union types to ensure that SidebarProps has either one or the other complete and valid shapes.

With these changes, not only does the TS error in Sidebar go away, but when you render it in another component you will get the expected proper TS checking. If type is "job" but the props prop does not have the expected shape of AddEditJobsProps, you will get an error. Try it for yourself in this sandbox:

https://codesandbox.io/s/youthful-fog-8lq41

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

Comments

1

I think the answer is that your "dynamic component" pattern here is making things difficult. It confusing for both humans and computers to understand, resulting in code that is hard to read and maintain, and that the Typescript compiler can't accurately check.

A better pattern to use here would be component composition. Make <Sidebar> more generic so that it doesn't care what children it renders, and only handles the open/toggle state.

export default const Sidebar: React.FC = ({children}) => {
  const { open, toggle } = useToggle();

  return (
    <RightSidebar open={open} toggleDrawer={toggle}>
      {children}
    </RightSidebar>
  );
}

Then, when you want to render a sidebar you just give it the children that you want it to wrap:

<Sidebar>
  <AddEditJobs {...addEditJobsProps} />
</Sidebar>

// or

<Sidebar>
  <AddEditCustomers {...addEditCustomersProps} />
</Sidebar>

You will get accurate and strict type checking (because TS will know the exact type of components and props) and the structure of your code will be more readable and easier to follow.

2 Comments

Actually the pattern you mentioned is how the app is currently designed. However the app is expanding and I'm finding the need for a dynamic sidebar that can be called based on context. The idea is to have useSidebar hook that can be called from any route/screen and then just pass the "type" and "props" and let the sidebar figure out the rest. I could use this with a switch statement so when you pass the type I just switch between the actual named components but that would still have the exact same problem meaning when I pass the type I won't be able to match it with the corresponding props.
Also I edited the question to show how I bypassed the error however still not able to match the type with the props which is what I'm looking for here.

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.