reactjstypescriptmapped-types

How to refactor long union type?


I want to create a React functional component which takes one of the predefined components as a prop and which logic depends on what component was passed in.

So these are the possible names:

enum ComponentName {
  ComponentA: "component.a",
  ComponentB: "component.b",
  ComponentC: "component.c",
  // etc
}

Now the hard part, I have a bunch of Component types, which names are all in the enum, like:

type ComponentA = {
  attributes: {
    // ...
  }
}

type ComponentB = {
  attributes: {
    // ...
  }
}

Now what I want to do is somehow to define type Component, so that:

type Component = 
| {
    id: string;
    name: ComponentName.ComponentA;
    component: ComponentA;
  }
| {
    id: string;
    name: ComponentName.ComponentB;
    component: ComponentB;
  }
| // ...

Is there a way to simplify this union type by mapping over ComponentName enum and matching enum prop to Component type name?


Solution

  • I think if you have some sort of mappings between component names and their attributes ComponentMappings, then you can create a union from that mapping using a helper function like

    type Values<T> = T[keyof T];
    

    Values generates a union type from the values of a mapped type or object.

    enum ComponentName {
      ComponentA = "component.a",
      ComponentB = "component.b",
      ComponentC = "component.c",
    }
    
    type ComponentMappings = {
      [ComponentName.ComponentA]: {
        attributes: {
          className: string;
        };
      };
      [ComponentName.ComponentB]: {
        attributes: {
          className: string;
          foo: number;
        };
      };
      [ComponentName.ComponentC]: {
        attributes: {
          className: string;
        };
      };
    };
    
    type Values<T> = T[keyof T];
    
    type Component = Values<{
      [K in ComponentName]: {
        id: string;
        name: K;
        component: ComponentMappings[K];
      };
    }>;
    
    const comopnentA: Component = {
      name: ComponentName.ComponentA,
      id: "component-a",
      component: {
        attributes: {
          className: "flex",
        },
      },
    };
    
    const componentB: Component = {
      name: ComponentName.ComponentB,
      id: "component-B",
      component: {
        attributes: {
          className: "flex",
          foo: 3,
        },
      },
    };
    

    TypesSript Playground

    Note that Prettify type in the playground does nothing and just makes it more readable when you hover on the type.