Creating a generic component map resolver for flexible applications

Currently, I am engaged in a project where the backend allows for the modeling of dynamic content that is later displayed as Components on the frontend.

Everything seems to be functioning well, except when dealing with models where the dynamic content consists of an array of union types that need to be resolved to components.


In regular JavaScript, handling this is straightforward without any concerns. However, I aim to statically type the component mapper.

My objective is to develop a mapper that can work with this syntax and automatically deduce type names (from keys) and properties (from values), while ensuring only ComponentType values are accepted:

// PageComponentMapper.ts
import componentMapperFactory from './componentMapperFactory'

import Hero from './Hero'
import InstagramFeed from './InstagramFeed'
import FrameBuilder from './FrameBuilder'

export default componentMapperFactory({ 
  Hero, 
  InstagramFeed, 
  FrameBuilder 
})

Sample Usage:

import { ComponentProps } from 'react'
import PageComponentMapper from './PageComponentMapper'

type PageComponentMapperProps = ComponentProps<typeof PageComponentMapper>
type PageContentItem = 
  PageComponentMapperProps['props'] & 
  { key: string } & 
  { type: PageComponentMapperProps['type'] }

Resulting Types:

type PageComponentMapperProps = {
  props: HeroProps | InstagramFeedProps | FrameBuilderProps,
  type: "Hero" | "InstagramFeed" | "FrameBuilder"
}

Progress So Far: View here

This implementation of the component mapper:

import { createElement, ComponentType } from "react";

interface ComponentResolverProps<
  ResolvableComponentTypeNames,
  ResolvableComponentPropTypes extends {}
> {
  type: ResolvableComponentTypeNames;
  props: ResolvableComponentPropTypes;
}

// How to infer type names and prop types from map?
export default function componentMapperFactory<
  AvailableComponentTypeNames extends string,
  AvailableComponentPropTypes
>(
  map: {
    [K in AvailableComponentTypeNames]: ComponentType<
      AvailableComponentPropTypes
    >
  }
) {
  return function resolveComponentFromMap({
    type,
    props
  }: ComponentResolverProps<
    AvailableComponentTypeNames,
    AvailableComponentPropTypes
  >) {
    const component = map[type];

    if (!component) {
      return null;
    }

    return createElement(component, props);
  };
}

Priorly, I used the approach where type names were not provided and defaulted to strings, which proved ineffective in distinguishing between typenames.


I also experimented with:

export default function componentMapperFactory<
  C extends { [key: string]: ComponentType }
>(
  map: {
    [N in keyof C]: C[N] extends ComponentType<infer P>
      ? (P extends unknown ? ComponentType<{}> : C[N])
      : never
  }
) {
  return function resolveComponentFromMap({
    type,
    props
  }: {
    type: keyof C;
    props: ComponentProps<C["string"]>;
  }) {
    const component = map[type];

    if (!component) {
      return null;
    }

    return createElement(component, props);
  };
}

However, this method failed to retain prop types, and the complex syntax made it challenging to navigate.

Explore the CodeSandbox of this approach: Visit here


In summary: how can I modify the componentMapperFactory so that I no longer require explicit type arguments like:

export default componentMapperFactory({ 
  Hero, 
  InstagramFeed, 
  FrameBuilder 
})

And achieve these resulting types for

ComponentProps<typeof PageComponentMapper>
?

type PageComponentMapperProps = {
  props: HeroProps | InstagramFeedProps | FrameBuilderProps,
  type: "Hero" | "InstagramFeed" | "FrameBuilder"
}

Answer №1

To successfully achieve your objective, you must define all component types and validate the props based on each component type. The key is to create a comprehensive map of all components and extract two crucial types:

  1. The type of possible components: essentially the keys in the map
  2. The type of component props for a specific component: this can be challenging because props valid for a 'Hero' component may not be suitable for an 'InstagramFeed' component. To address this issue, utilize a generic type tied to the component type that will determine the appropriate props type.

PageComponentMapper.ts


import Hero from "./Hero";
import FrameBuilder from "./FrameBuilder";
import InstagramFeed from "./InstagramFeed";
import componentMapperFactory from "./componentMapperFactory";

export default componentMapperFactory({
  Hero,
  FrameBuilder,
  InstagramFeed
});

componentMapperFactory.ts

import { createElement } from "react";

interface MapType {
  // necessary for Parameters<T[K]> functionality
  [key: string]: (...args: any) => any;
}

export default function componentMapperFactory<T extends MapType>(
  contentTypeMap: T
) {
  // 1. K represents any key from the map
  return function<K extends keyof T>({
    type,
    props
  }: {
    type: K;
    // 2. Retrieve the component function with key 'K' and use the first parameter's type as props
    props: Parameters<T[K]>[0];
  }) {
    const Component = contentTypeMap[type];
    if (!Component) {
      return null;
    }
    return createElement(Component, props);
  };
}

For further information, refer to the TS documentation on generics and Parameters

Answer №2

Let me give it a shot, and please let me know if I'm off track with this.

A while back, I developed a Typescript type definitions generator specifically for Contentful. This generator creates an interface and implementation for each Contentful content-type based on a JSON file representing the space. You can check out the code here.

My approach involved creating a "TypeDirectory" interface to convert an Interface into an Implementation. By referencing various fields of the type directory using a common key, I was able to achieve this.

export interface TypeDirectory {
  'campus': C.ICampus;
  'codeButton': C.ICodeButton;
  'card': C.ICard;
  'campus-page': C.ICampusPage;
  'dropdownMenu': C.IDropdownMenu;
  'faq': C.IFaq;
  ...
}

export interface ClassDirectory {
  'campus': C.Campus;
  'codeButton': C.CodeButton;
  'card': C.Card;
  'campus-page': C.CampusPage;
  'dropdownMenu': C.DropdownMenu;
  'faq': C.Faq;
  ...
}

/** Wraps a raw JSON object representing a Contentful entry with the appropriate Class implementation for that type. */
export function wrap<CT extends keyof TypeDirectory>(entry: TypeDirectory[CT]): ClassDirectory[CT] {
  const id = entry.sys.contentType.sys.id
  switch (id) {
    case 'campus':
      return new C.Campus(entry)
    case 'codeButton':
      return new C.CodeButton(entry)
    case 'card':
      return new C.Card(entry)
    ...
    default:
      throw new Error('Unknown content type:' + id)
}

I've applied a similar strategy in other scenarios where properties need to be extracted from a union type. You could implement a Directory type like this:

interface TypeDirectory {
    "Hero": { props: HeroProps, impl: Hero },
    "InstagramFeed": { props: InstagramFeedProps, impl: InstagramFeed }
}

function componentMapperFactory<K extends keyof TypeDirectory>(key: K): (props: TypeDirectory[K]['props']) => TypeDirectory[K]['impl'] {
    return function (props: any) {
        // React.createElement...
    } as any
}

const heroFactory = componentMapperFactory('Hero')
const feedFactory = componentMapperFactory('InstagramFeed')

type AnyTypeKey = keyof TypeDirectory
const whateverKey: AnyTypeKey = '...' as any // get from API
const whateverProps: TypeDirectory[AnyTypeKey]['props'] = JSON.parse('' /* get from API */)
const whateverFactory = componentMapperFactory<AnyTypeKey>(whateverKey)
const whateverType: TypeDirectory[AnyTypeKey]['impl'] = whateverFactory(whateverProps)

https://i.stack.imgur.com/7wP1J.png https://i.stack.imgur.com/peqx5.png

Playground Link

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

What is the method to alter the color of an SVG source within an image tag?

I am currently working on a component that involves changing the color of an SVG icon from black to white when a color prop is given. export class CategoryIconComponent extends React.Component { static displayName = 'CategoryIcon'; state = ...

Adjust padding for smaller devices in React using Material UI

In my grid layout, I have columns set to 3,6,3 and a spacing of 3 between them. On larger screens, the spacing between grids looks fine. However, as the screen size decreases, the spacing remains the same which is not visually appealing. What I am aiming ...

Angular 2 approach to retrieving items from an Observable<Xyz[]>

After reviewing the Typescript code in an Angular 2 service: getLanguages () { return this.http.get(this._languagesUrl) .map(res => <Language[]> res.json().data) .catch(this.handleError); I'm encountering a challenge whe ...

Update nested child object in React without changing the original state

Exploring the realms of react and redux, I stumbled upon an intriguing challenge - an object nested within an array of child objects, complete with their own arrays. const initialState = { sum: 0, denomGroups: [ { coins: [ ...

Only function components can utilize hooks within their body. The useState functionality is currently not functioning as expected

Currently working on a GatsbyJS project and attempting to utilize a Hook, however encountering an error message. Initially, I decided to remove the node_modules folder and package.json.lock file, then executed npm install again, unfortunately without reso ...

Converting JSON objects into TypeScript classes: A step-by-step guide

My challenge lies in converting Django responses into Angular's User array. This conversion is necessary due to variations in variable names (first_name vs firstName) and implementing specific logic within the Angular User constructor. In simple term ...

Combining Different Types of Errors

Can TypeScript's type system be exploited to provide additional information from a repository to a service in case of errors? I have a service that needs a port for a repository (Interface that the Repository must implement), but since the service mu ...

Utilize setState to showcase data fetched from AJAX request

Currently, I am in the process of developing a web application using the GitHub search API. My goal is to have detailed information about each repository displayed below its corresponding entry. Specifically, I want the content retrieved from the AJAX re ...

Looking to retrieve selections when the inputValue changes in react-select?

I'm working with a react-select component and I would like to implement a feature where an API request is triggered as soon as the user starts typing in the react-select field. This request should fetch items related to the keyword entered by the user ...

Invalid anchorEl prop given for MUI component

Currently, I am in the process of developing a mobile menu feature, where upon clicking the menu icon, the menu will be displayed. The code snippet for this functionality is as follows: const StyledMenu = withStyles({ paper: { border: '1px soli ...

TypeError thrown by Mapbox markers

Looking to incorporate markers into my map using Mapbox. Below is the Angular TypeScript code I am working with: export class MappViewComponent implements OnInit { map: mapboxgl.Map; lat = 41.1293; lng = -8.4464; style = "mapbox://styles/mapb ...

How to hide the border on the left and right sides of a phone number input in Tail

I'm currently working on a React component that allows users to select a country code and enter a phone number. However, I'm facing a challenge in trying to hide the left border of the country code select input and the right border of the phone n ...

Creating a dynamic return statement in typescript: A step-by-step guide

I am looking to dynamically set a return value to a variable based on values retrieved from an API. The current function in question uses static values: this.markDisabled = (date: NgbDate) => { return (this.calendar.getWeekday(date) !== 5 && ...

The component `style` requires a callback function as a getter for its configuration, but `undefined` was received instead. Remember to always capitalize component names at the beginning

When working with the MUI library in React Native, I encountered an issue: ERROR Invariant Violation: View config getter callback for component style must be a function (received undefined). Make sure to start component names with a capital letter. I ha ...

Issue encountered when importing a font in TypeScript due to an error in the link tag's crossorigin

How do I troubleshoot a TypeScript error when importing a custom font, such as a Google font? <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> Below is the specific error message: Type 'boolean' is ...

React's setState function is overriding the current state

I am facing an issue with updating a single value in my state object. In a form, there is one input field for the name and I have implemented the following code in its onChange method: onChangeUpdateForm(e) { this.setState({ currentService ...

I am looking to replicate a DOM element using Angular 4

I am interested in creating a clone of a DOM element. For example, if I have the following structure: <div> <p></p> <p></p> <p></p> <p></p> <button (click)="copy()"></button> & ...

How can I extract just the initial 2 letters of a country name using AmCharts maps?

Having trouble with Amcharts maps. I have a map that displays countries as United States, but I only want them to show as US. Is there a country formatter available for this issue? Any help is appreciated. ...

Enforce directory organization and file naming conventions within a git repository by leveraging eslint

How can I enforce a specific naming structure for folders and subfolders? I not only want to control the styling of the names (kebab, camel), but also the actual names of the folders and files themselves. For example, consider the following paths: ./src/ ...

Which is the best choice for a large-scale production app: caret, tilde, or a fixed package.json

I am currently managing a sizeable react application in production and I find myself undecided on whether it is more beneficial to use fixed versions for my packages. Some suggest that using the caret (^) is a safer route, but I worry that this could poten ...