0

首先 - 对不起,很长的解释,但我不确定如何在没有它的情况下足够清楚地表达我想要的东西。但是......我正在 React 中制作一个 Table 组件(使用打字稿)。我在想类似的事情:

const data = [
 { id: 1, name: "Peter", age: 43, country: "USA" },
 { id: 2, name: "Stewie", age: 2, country: "UK" },
 { id: 3, name: "Brian", age: 10, country: "Spain" },
]

<Table
  data={data}
  headers={[
    {
      title: 'Name',
      value: 'name'
    },
    {
      title: 'Age (In Years)',
      value: 'age'
    },
  ]} />

所以我想确保选择的值仅限于原始数据的道具。我用这个界面做的:

interface HeaderData<ItemKey> {
  accessor: ItemKey;
  title: string;
}

interface Props<Item> {
  data: Array<Item>;
  headers: Array<HeaderData<keyof Item>>;
}

效果很好。所以现在问题来了。我想包括添加新行的能力。因此,为了实现这一点,我正在为每一列呈现一个文本框,并且我想跟踪使用 useState 为列输入的数据。所以我可以写以下内容:

const [data, setData] = useState<Partial<Item>>({});

然后边走边设置数据。但我真的很想将数据对象基于标题,这样如果标题只是示例中的名称和年龄。我只有 { name: string, age: string } 的数据对象

所以本质上 useState 默认值是:

const [data, setData] = useState(
  headers.reduce((curr, next) => ({ ...curr, [next.value]: "" }), {})
);

当我查看打字稿将其解释为 {} 时 - 但是当我后来尝试使用 data[header.value] 访问它时,它会抱怨

Type 'keyof T' cannot be used to index type '{}'. 

如果我这样做也无济于事: data[header.value.toString()] 或类似的东西也没有。

我需要设置什么类型才能对“数据”施加约束,以便我只获得“标题”值?

谢谢阅读 ;)

编辑:感谢@JonathanHamel 指出一个明显的错误。但是类型问题仍然存在。

4

2 回答 2

3

这是我对您的问题的理解。您的初始数据集包括四个属性idnameagecountry。您的表仅显示其中两个属性:nameage。您希望能够向表中添加行并要求添加的对象只需要两个可见属性nameage而不是全部四个。

如果Item是整个对象,那么可见部分是Pick<Item, 'name' | 'age'>。但是我们希望能够推断出从headers对象中选取了哪些属性。所以我们将使用泛型K extends keyof Item来描述可见列。我们data只需要可见的属性Pick<Item, K>

根据您的评论进行编辑:行的状态将存储在表外,但我们通过单击“添加”创建的行的状态将存储在表内。string由于组件的限制,添加行的所有值都将采用格式input。所以添加的行是一个具有键K和值string的对象Record<K, string>

interface HeaderData<ItemKey> {
    accessor: ItemKey;
    title: string;
}

interface Props<Item, K extends keyof Item> {
    data: Array<Pick<Item, K>>;
    headers: Array<HeaderData<K>>;
    onAddRow: (row: Record<K, string>) => void;
}

const Table = <Item, K extends keyof Item>({ data, headers, onAddRow }: Props<Item, K>) => {
    // create a new row object with empty string properties matching the header accessors
    // need to assert type or else it thinks the keys are just string
    const emptyRow = Object.fromEntries(headers.map(h => [h.accessor, ""])) as Record<K, string>;

    // the contents of the add row section
    const [nextRow, setNextRow] = useState(emptyRow);
    // whether or not add row section is expanded
    const [isAdding, setIsAdding] = useState(false);

    const onClickAdd = () => {
        // enter edit mode
        setIsAdding(true);
    }

    const onClickSave = () => {
        // push changes to parent
        onAddRow(nextRow);
        // exit edit mode
        setIsAdding(false);
        // reset nextRow
        setNextRow(emptyRow);
    }

    return (
        <table>
            <thead>
                <tr>
                    {headers.map(header => (
                        <th scope="col">{header.title}</th>
                    ))}
                </tr>
            </thead>
            <tbody>
                {data.map(row => (
                    <tr>{/**... */}</tr>
                ))}
            </tbody>
        </table>
    )
};

export const Test = () => {

    const [data, setData] = useState(initialData);

    const onAddRow = ({ name, age }: { name: string; age: string }) => {
        setData(data => [...data, {
            name,
            // convert string to number
            age: parseInt(age),
            // fill in missing properties
            country: "USA",
            id: -1,
        }])
    }

    return (
        <Table
            data={data}
            headers={[
                {
                    title: 'Name',
                    accessor: 'name'
                },
                {
                    title: 'Age (In Years)',
                    accessor: 'age'
                },
            ]}
            onAddRow={onAddRow}
        />
    )
}

实际上,我对上面的代码得到正确的推断感到有点惊讶,因为我们的类型不需要headers详尽无遗(所以K可能只是keyof Item而不是子集)。但它似乎工作!

中的Table元素Test将类型推断为:

  • Item{ id: number; name: string; age: number; country: string; }
  • K"name" | "age"

打字稿游乐场链接

于 2021-04-14T01:05:52.803 回答
0

如果您的数据和标题是动态的,那么您无法知道静态类型是什么(string当然,标题值除外)。

对于动态数据,通常您希望从Record<PropertyKey, unknown>. 在这种情况下,您可能希望强制始终存在一个id字段为number- 在这种情况下,您可以从 扩展Record<PropertyKey, unknown> & { id: number }

然后您的标题类型将从Record<keyof Omit<Row, "id">, string>

这是我玩了之后想出的:

type Row<T extends Record<PropertyKey, unknown>> = T & { id: number }

type TableHeaders<T extends Record<PropertyKey, unknown>> =
  Record<keyof Omit<Row<T>, "id">, string>

type ExampleItem = { name: string; age: number; country: string }

type ExampleRow = Row<ExampleItem>

const data: ExampleRow[] = [
 { id: 1, name: "Peter", age: 43, country: "USA" },
 { id: 2, name: "Stewie", age: 2, country: "UK" },
 { id: 3, name: "Brian", age: 10, country: "Spain" },
]

declare const exampleHeaders: TableHeaders<ExampleRow>
// { name: string, age: string; country: string }

// To bring it back to your props interface, it may look like this:
interface Props<Item extends Record<PropertyKey, unknown>> {
  data: Array<Row<Item>>;
  headers: TableHeaders<Item>;
}

declare const props: Props<ExampleItem>

Here's a playground link: https://www.typescriptlang.org/play?jsx=0#code/C4TwDgpgBASg9gdwDwBUoQB7AgOwCYDOsEAxnAE55IAK5ck5oA0hCADRQCuOA1jojgB8gqAF4oaAGRQA3lACWeAFxQcnALYAjCOSgBfAFAHQkCQENNAGwgAJCGbw6CqdFlyFiZSjToNmrDm4+AWExTwoqHlY4ADMoAHl1eWAkeGQUQQ4AIkUszKgCYHJ5HABzQSMTaABRDDN1MGsASWx1MLkceogVQuKygG4oM1Lu1Q1tckGybiKQHqKS0v1K8Bq6hus0sLSkWvrGiBaIdQqDMhxCqDwzYDMVPY2INIBtAF0w54NZBWUoAEYOJ11KMstQINhyFkOMNRgAWADMHGmOFmKiyAFUAMoAQSy+jYXzkihUACZAV00ZjsAh5BAoUMRqSkXAZuQ5lAMUw8XoCd9iVBEaoKRyAELFMw4ekwlR-AAMzNZ7KymLAZhK3IJryMjhIljM5Gg50umH21jsDicKhQFjN9kc5GcDwOaREAHpXd8gaNeotoYyCgsBlBkaiA30loYDEA

于 2021-04-13T17:23:31.393 回答