键入组件图解析器通用组件

时间:2019-11-07 16:14:37

标签: reactjs typescript generics polymorphism

我正在研究一个项目,该项目可以在后端对动态内容进行建模,然后在前端将这些内容呈现为组件。

除了在模型中使用动态内容是联合类型的数组然后应分解为组件的模型之外,它在大多数情况下都有效。


使用普通JavaScript很容易,因为没有后顾之忧,但是我希望静态地键入组件映射器。

我的目标是创建一个映射器,使其可以使用此语法并推断类型名称(从键)和属性(从值),同时防止除ComponentType以外的值:

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

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

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

用法:

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

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

制作中

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

我走了多远? https://codesandbox.io/s/relaxed-star-6tqzz

使用此组件映射器实现:

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);
  };
}

以前,我实际上没有提供类型名称,而是默认为字符串,这无法帮助我识别类型名称。


我也有以下想法:

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);
  };
}

但是这丢失了prop类型,我不知道为什么,而且在复杂的语法方面也很疯狂。

此方法的CodeSandbox:https://codesandbox.io/s/unruffled-hill-i5cbr


因此,总结一下:如何转换componentMapperFactory,以便可以在没有显式类型参数的情况下使用它:

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

并将这些类型作为ComponentProps<typeof PageComponentMapper>的结果吗?

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

2 个答案:

答案 0 :(得分:1)

我将对此采取行动,让我知道这是朝错误的方向前进。

我为Contentful编写了一个Typescript类型定义生成器,该生成器基于表示该空间的JSON文件为每个Contentful内容类型创建了一个接口和实现。 You can see it here

我将接口转换为实现的方法是生成“ TypeDirectory”接口。使用此接口,我可以基于一个公共密钥引用类型目录的各个字段。

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 Class implementation appropriate 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)
}

我在其他地方也需要使用这种方法,在这些地方我需要从联合类型中提取属性。因此,您可以使用类似于以下的目录类型:

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)

The resulting Typescript inferred types whateverType inferred type

Playground Link

答案 1 :(得分:1)

您的目标是定义所有组件类型,并根据组件类型验证道具。您只需要定义所有组件的映射,然后从该映射中提取2种类型:

  1. 可能的组件类型:基本上是地图键
  2. 组件的类型给定组件的道具:这是棘手的部分,因为对“英雄”组件有效的道具可能对“ InstagramFeed”组件无效。表达这种方式的方法是使用基于组件类型的通用类型,该类型将返回正确的道具类型。

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 {
  // this is required to make Parameters<T[K]> work
  [key: string]: (...args: any) => any;
}

export default function componentMapperFactory<T extends MapType>(
  contentTypeMap: T
) {
  // 1. K can be any key from the map
  return function<K extends keyof T>({
    type,
    props
  }: {
    type: K;
    // 2. get the component function with key 'K', use the type of its 1st parameter for props
    props: Parameters<T[K]>[0];
  }) {
    const Component = contentTypeMap[type];
    if (!Component) {
      return null;
    }
    return createElement(Component, props);
  };
}

有关更多信息,请检查genericsParameters的TS文档