首页 > 解决方案 > 根据数据中的类型字段将数据数组映射到特定的反应组件

问题描述

我们正在尝试将具有一组特定键和值的记录映射到特定的反应组件。这些值可以是任意的,但所有值都必须具有type来自特定类型集的属性。

type我们已经走了很长一段路来构建我们的值,以便我们可以通过它的属性来查找值类型。我们的想法是,如果我们能做到这一点,我们可以使用type属性,查找值类型并将该值类型映射到特定的反应组件。

但它没有用。

我想我可以通过将代码与解释性注释混合来最好地解释这个问题,所以我将从这里继续使用代码块:

你可以在这里找到作为 TypeScript Playground 的完整代码。

import React from "react"

/**
 * This is our data modeling
 * 
 * The goal here is to create a base model (called "Item") that can be extended with additional
 * data but must implement the basic Item fields (which the property "type").
 * We also wanted to have a map of the types that we have to their models. The idea for this was 
 * to make it work for us to map types of models to their specific variants
 */

// Here we have a couple of base types
type ItemType = "t1" | "t2" | "t3"

// We have a base model "item" that has a field "type" which is of ItemType
interface Item<T extends ItemType> {
    type: T;
}

// This defines a "Record<>" that maps every ItemType as a key to an Item of that type as a value
export type RecordOfItems = { [K in ItemType]: Item<K> };

// This helper lets us extend the basic RecordOfItems in a typesafe way:
// With this we can extend the RecordOfItems and make sure each key is still an
// ItemType and the value is an extension of the according Item.
// You cannot add other keys and you cannot use other values.
export type Items<T extends RecordOfItems> = T;

// This is the specific implementation of the RecordOfItems, we have t1-t3 as keys,
// each one is mapping to an item but extending it with their own props
type ItemMap = Items<{
    t1: { type: "t1"; foo: "foo"  };
    t2: { type: "t2";  bar: "bar" };
    t3: { type: "t3";  baz: "baz" };
}>;

// Now these were definitions of the possible data that we have, the
// data structure that we have is an array of items. The reason we create an
// ItemMap and now have an array of its values is the idea that we could map
// the ItemTypes which we can find in the `type` field to the according definition
// of the model when we read the data and need to look up which array item implements
// which model
const data: ItemMap[keyof ItemMap][] = [
    { type: "t1", foo: "foo"  },
    { type: "t2",  bar: "bar" },
    { type: "t3",  baz: "baz" },
    { type: "t2",  bar: "bar" },
    { type: "t1", foo: "foo"  },
    { type: "t2",  bar: "bar" },
    { type: "t2",  bar: "bar" },
    { type: "t3",  baz: "baz" },
]

/**
 * This is our react implementation where we map our data to react components
 */

// We create another Record that maps the item types to React components
type ComponentMap = { [T in ItemType]: React.VFC<{ item: ItemMap[T] }> }


// This is the implementation of that
const componentMap: ComponentMap = {
  t1: ({ item }) => <p>{ item.foo }</p>,
  t2: ({ item }) => <p>{ item.bar }</p>,
  t3: ({ item }) => <p>{ item.baz }</p>,
}

// Now all of that type foo bar was to make this work: get an item, look
// into its type and return the according react components and pass the item to it
// The idea was that TypeScript should understand that when we get an item of the type T
// TS could understand that there is a corresponding Component for that that matches the item
function getElement<T extends ItemType>(item: Item<T>) {
    const Component = componentMap[item.type];
    return <Component item={item} />;
}

// but it does not work. the Component cannot be properly mapped, TS does not even
// seem to be convinced the `Component` we get is one of the components of the map
// and cannot map it to the item as a prop

标签: reactjstypescript

解决方案


我花了一点时间)

首先,item: Item<T>不能作为 Component 的 props,因为它只有typeprops。Item类型不包含foo//barbaz

我添加了一个回调getElement- 因为键入其他所有内容要容易得多。

我还添加了 ItemMap - const

这是我的解决方案:

import React from "react"

/**
 * This is our data modelling
 * 
 * The goal here is to create a base model (called "Item") that can be extended with additional
 * data but must implement the basic Item fields (which the property "type").
 * We also wanted to have a map of the types that we have to their models. The idea for this was 
 * to make it work for us to map types of models to their specific variants
 */

// Here we have a couple of base types
type ItemType = "t1" | "t2" | "t3"

// We have a base model "item" that has a field "type" which is of ItemType
interface Item<T extends ItemType> {
    type: T;
}

// This defines a "Record<>" that maps every ItemType as a key to an Item of that type as a value
export type RecordOfItems = { [K in ItemType]: Item<K> };

// This helper lets us extend the basic RecordOfItems in a typesafe way:
// With this we can extend the RecordOfItems and make sure each key is still an
// ItemType and the value is an extension of the according Item.
// You cannot add other keys and you cannot use other values.
export type Items<T extends RecordOfItems> = T;

// This is the specific implementation of the RecordOfItems, we have t1-t3 as keys,
// each one is mapping to an item but extending it with their own props
type ItemMap = Items<{
    t1: { type: "t1"; foo: "foo" };
    t2: { type: "t2"; bar: "bar" };
    t3: { type: "t3"; baz: "baz" };
}>;

// If you want pass only Item as argument, we should also have a map
const ItemMap:ItemMap = {
    t1: { type: "t1", foo: "foo" },
    t2: { type: "t2", bar: "bar" },
    t3: { type: "t3", baz: "baz" },
}
// Now these were definitions of the possible data that we have, the
// data structure that we have is an array of items. The reason we create an
// ItemMap and now have an array of its values is the idea that we could map
// the ItemTypes which we can find in the `type` field to the according definition
// of the model when we read the data and need to look up which array item implements
// which model
const data: ItemMap[keyof ItemMap][] = [
    { type: "t1", foo: "foo" },
    { type: "t2", bar: "bar" },
    { type: "t3", baz: "baz" },
    { type: "t2", bar: "bar" },
    { type: "t1", foo: "foo" },
    { type: "t2", bar: "bar" },
    { type: "t2", bar: "bar" },
    { type: "t3", baz: "baz" },
]

type Data = typeof data

/**
 * This is our react implementation where we map our data to react components
 */

// We create another Record that maps the item types to React components
type ComponentMap = { [T in ItemType]: React.VFC<{ item: ItemMap[T] }> }


// This is the implementation of that
const componentMap: ComponentMap = {
    t1: ({ item }) => <p>{item.foo}</p>,
    t2: ({ item }) => <p>{item.bar}</p>,
    t3: ({ item }) => <p>{item.baz}</p>,
}

function foo<T extends ItemType>(item: Item<T>, callback: (comp: ComponentMap[T], props: ItemMap[T]) => JSX.Element): JSX.Element {
    const comp = componentMap[item.type];
    return callback(comp, ItemMap[item.type])
}

const Comp = foo({ type: 't1' }, (Comp, props) => <Comp item={props} />) // ok
const Comp2 = foo({ type: 't1' }, (Comp, props) => <Comp item={{ type: "t1", bar: "foow" }} /> //error

推荐阅读