4

for a project, I want to dynamically render different components. I'll get an array with different types of objects, and when I map over that array I want to render a component based on a prop of that object. I could use an if/else || switch statement, but with more and more different types of objects, these statements would be very long, so I want to use a different approach, meaning use the prop as a key of an object, which then defines the component which should get rendered.

App.tsx

import instructions, { Instruction } from "./instructions";
import products, { Product } from "./products";

// COMPONENTS
interface ProductCompProps {
  content: Product;
}
const ProductComponent: React.FC<ProductCompProps> = ({ content }) => {
  return <h1>Product Component: {content.name}</h1>;
};

interface InstructionCompProps {
  content: Instruction;
}
const InstructionComponent: React.FC<InstructionCompProps> = ({ content }) => {
  return <h1>Product Component: {content.label}</h1>;
};
//

const datas = [...products, ...instructions];

interface Components {
  [key: string]: React.FC<ProductCompProps> | React.FC<InstructionCompProps>;
}
const components: Components = {
  ProductComponent,
  InstructionComponent,
};

const App: React.FC = () => {
  return (
    <div>
      {datas.map((data) => {
        const Component = components[data.component];
        return <Component content={data} />;
      })}
    </div>
  );
};

export default App;

products.ts

export interface Product {
  id: string;
  name: string;
  component: string;
}

const products: Product[] = [
  {
    id: "1",
    name: "Product name One",
    component: "ProductComponent",
  },
  {
    id: "2",
    name: "Product name Two",
    component: "ProductComponent",
  },
];

export default products;

instructions.ts

export interface Instruction {
  id: string;
  label: string;
  component: string;
}

const instructions: Instruction[] = [
  {
    id: "1",
    label: "Product name One",
    component: "InstructionComponent",
  },
  {
    id: "2",
    label: "Product name Two",
    component: "InstructionComponent",
  },
];

export default instructions;

So this custom component "Component" could be one of X components, each with its own props.

But Typescript is giving me the following error for the content prop:

Type 'Product | Instruction' is not assignable to type 'Product & Instruction'.
  Type 'Product' is not assignable to type 'Product & Instruction'.

What I understand ist, that you can only index an object with certain types, like string or number. In my case when I set a string key:

interface Components {
  [key: string]: React.FC<ProductCompProps> | React.FC<InstructionCompProps>;
}

the indexing is working, but then I can't understand why the compiler is mixing the types of the component prop...

I would be very thankful for any help. Please understand that I'm very new to typescript, so I still struggle with generics and advanced typescript stuff.

1
  • Why not any type if types are random in each object? Commented May 26, 2021 at 9:56

2 Answers 2

3

Typing your datas array:

const datas: (Product | Instruction)[] = [...products, ...instructions];

So Typescript is confused because it doesn't know if an element of datas is a reference to a Product or Instruction as you iterate over it. This means Component is typed to be Product & Instruction and it complains when you pass it through props that don't have both of those properties.

The simplest way to fix this is just going to be to type either that component or the data as any, or do some if/else to cast data to the intended type as there's no way typescript is going to be able to figure out this at compile time if the array is in any way generated.

This might be better written in the following:

Components.tsx

// These now take a subset of props without the component
type ProductProps = {
  id: string;
  name: string;
}
const ProductComponent: React.FC<ProductProps> = (props) => {
  return <h1>Product Component: {props.name}</h1>;
};

type InstructionProps {
  id: string;
  label: string;
}
const InstructionComponent: React.FC<InstructionProps> = (props) => {
  return <h1>Product Component: {props.label}</h1>;
};

export {
    ProductProps, 
    ProductComponent,
    InstructionProps,
    InstructionComponent
};

renderable.ts

/* A generic for a "renderable" object, with props + component in one object */
type Renderable<Props> = Props & {
    Component: React.FC<Props>
};

export { Renderable };

products.ts

type Product = Renderable<ProductProps>;

const products: Product[] = [
  {
    id: "1",
    name: "Product name One",
    Component: ProductComponent, // This is the actual component, not a string
  },
  {
    id: "2",
    name: "Product name Two",
    Component: ProductComponent,
  },
];

export default products;

export { Product };

instructions.ts

type Instruction = Renderable<InstructionProps>;

const instructions: Instruction[] = [
  {
    id: "1",
    label: "Product name One",
    Component: InstructionComponent,
  },
  {
    id: "2",
    label: "Product name Two",
    Component: InstructionComponent,
  },
];

export default instructions;

export { Instruction };

Now putting it all together...

App.tsx

const datas: (Product | Instruction)[] = [...products, ...instructions];

const App: React.FC = () => {
  return (
    <div>
      {datas.map((data) => {
        // We now directly take the props and component from the array rather than indexing it.
        const {
            Component, 
            ...passThroughProps
        } = data;
        return <Component {...(passThroughProps as any)} />;
      })}
    </div>
  );
};

export default App;

The reason this works is now we've told our app component a generic structure it can use to render things - That our data object contains a Component prop, and the rest of the object contains the prop values for that component.

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

9 Comments

First of all, thank you for the response! After setting up everything as you said, I'll get an error in my datas array: ` Type '(Product | Instruction)[]' is not assignable to type 'Renderable<ProductCompProps | InstructionCompProps>[]'. Type 'Product | Instruction' is not assignable to type 'Renderable<ProductCompProps | InstructionCompProps>'. `
Sorry, made a typo! const datas: Renderable<ProductProps | InstructionProps>[] = [...products, ...instructions]; should instead be const datas: (Product | Instruction)[] = [...products, ...instructions];. and I any typed the passThroughProps.
Here's a tsplayground link and I've updated the answer. pastebin.com/FFQAm6bf. (In a pastebin due to character limit)
Thanks again for the help. So datas actually knows its type, which is exactly what you defined in your comment above. When I correct the datas type I'll basically get the same error as before: const Component: React.FC<ProductCompProps> | React.FC<InstructionCompProps> Type '{ id: string; name: string; } | { id: string; label: string; }' is not assignable to type 'IntrinsicAttributes & ProductCompProps & { children?: ReactNode; } & InstructionCompProps'. Property 'label' is missing in type '{ id: string; name: string; }' but required in type 'InstructionCompProps'.
Hi Willo! It's an intersection type - Here's the handbook. typescriptlang.org/docs/handbook/2/…
|
1

I could figure out a solution to this problem, for everyone who is interested: With assertion, you can tell the compiler, that the props are not an intersection of Product and Instruction.

const Component = components[data.component] as React.FC<{ content: Product | Instruction }>;

Also, you can modify the type of the interface:

export interface Product {
  id: string;
  name: string;
-  component: string;
+ component: "ProductComponent";
}

The same goes for interface Instruction.

So we can get rid of this (ugly?) interface:

interface Components {
    [key: string]: FC<ProductProps> | FC<InstructionProps>;
}

- const components: Components = {
+ const components = {
  ProductComponent,
  InstructionComponent,
};

Hope this will help someone else in the future. cheers

Comments

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.