React和TypeScript:如何在回调处理程序中使用泛型?

huangapple go评论64阅读模式
英文:

React and TypeScript: How to use generics for callback handlers?

问题

I have the following Table component in React which I need to implement as generic as possible with TypeScript. While the TableItem is required to have an id property, it can also have other properties (but instead of using a Record or any, I want to infer the types):

import * as React from "react";

export type TableItem = {
  id: string;
};

export type Column = {
  title: string;
  getContent: <T extends TableItem>(tableItem: T) => React.ReactNode; // <--- HOW TO MAKE GENERIC FOR PERSON TYPE IN APP COMPONENT
};

type TableProps = {
  data: any; // <--- HOW TO TYPE AS GENERIC?
  columns: Column[];
};

const Table: React.FC<TableProps> = ({ data, columns }) => {
  return (
    <table>
      <th>{columns.map((column: Column) => column.title)}</th>

      {data.map((tableItem: any) => (
        <tr key={tableItem.id}>
          {columns.map((column: Column) => column.getContent(tableItem))}
        </tr>
      ))}
    </table>
  );
};

export default Table;

My question: When using the Table component with its columns and its onChange callback handler within each column, I want to be able to have a typed tableItem in there. But it's not working as I am expecting:

import * as React from "react";

import Table, { Column, TableItem } from "./Table";

type Person = {
  id: string;
  name: string;
  age: number;
};

const COLUMNS: Column[] = [
  {
    title: "Name",
    // NOT WORKING THIS WAY           v
    getContent: (tableItem: TableItem & Person) => {
      return <span style={{ color: "red" }}>{tableItem.name}</span>;
    },
  },
  {
    title: "Age",
    // NOT WORKING THIS WAY           v
    getContent: (tableItem: TableItem & Person) => {
      return <span>{tableItem.age}</span>;
    },
  },
];

const App = () => {
  const data: Person[] = [
    {
      id: "1",
      name: "David",
      age: 20,
    },
    {
      id: "2",
      name: "Fiona",
      age: 24,
    },
  ];

  return <Table data={data} columns={COLUMNS} />;
};

export default App;

How can I fix all the code that got comments in the App and Table component? Full example over here.

英文:

I have the following Table component in React which I need to implement as generic as possible with TypeScript. While the TableItem is required to have a id property, it also can have other properties (but instead of using a Record or any, I want to infer the types):

import * as React from &quot;react&quot;;

export type TableItem = {
  id: string;
};

export type Column = {
  title: string;
  getContent: &lt;T extends TableItem&gt;(tableItem: T) =&gt; React.ReactNode; // &lt;--- HOW TO MAKE GENERIC FOR PERSON TYPE IN APP COMPONENT
};

type TableProps = {
  data: any; // &lt;--- HOW TO TYPE AS GENERIC?
  columns: Column[];
};

const Table: React.FC&lt;TableProps&gt; = ({ data, columns }) =&gt; {
  return (
    &lt;table&gt;
      &lt;th&gt;{columns.map((column: Column) =&gt; column.title)}&lt;/th&gt;

      {data.map((tableItem: any) =&gt; (
        &lt;tr key={tableItem.id}&gt;
          {columns.map((column: Column) =&gt; column.getContent(tableItem))}
        &lt;/tr&gt;
      ))}
    &lt;/table&gt;
  );
};

export default Table;

My question: When using the Table component with its columns and its onChange callback handler within each column, I want to be able to have a typed tableItem in there. But it's not working as I am expecting:

import * as React from &quot;react&quot;;

import Table, { Column, TableItem } from &quot;./Table&quot;;

type Person = {
  id: string;
  name: string;
  age: number;
};

const COLUMNS: Column[] = [
  {
    title: &quot;Name&quot;,
    // NOT WORKING THIS WAY           v
    getContent: (tableItem: TableItem &amp; Person) =&gt; {
      return &lt;span style={{ color: &quot;red&quot; }}&gt;{tableItem.name}&lt;/span&gt;;
    },
  },
  {
    title: &quot;Age&quot;,
    // NOT WORKING THIS WAY           v
    getContent: (tableItem: TableItem &amp; Person) =&gt; {
      return &lt;span&gt;{tableItem.age}&lt;/span&gt;;
    },
  },
];

const App = () =&gt; {
  const data: Person[] = [
    {
      id: &quot;1&quot;,
      name: &quot;David&quot;,
      age: 20,
    },
    {
      id: &quot;2&quot;,
      name: &quot;Fiona&quot;,
      age: 24,
    },
  ];

  return &lt;Table data={data} columns={COLUMNS} /&gt;;
};

export default App;

How can I fix all the code that got comments in the App and Table component? Full example over here.

答案1

得分: 2

你可以将通用的部分提升到类型定义中:

export type TableItem = {
  id: string;
};

export type Column<T extends TableItem> = {
  title: string;
  getContent: (tableItem: T) => React.ReactNode;
};

type TableProps<T extends TableItem> = {
  data: T[];
  columns: Column<T>[];
};

不幸的是,这意味着你不能再使用 FC,因为泛型定义将出现在 lambda 函数上:

const Table = <T extends TableItem>({ data, columns }: TableProps<T>): ReactNode | null => {
  return (
    <table>
      <th>{columns.map((column: Column<T>) => column.title)}</th>

      {data.map((tableItem: T) => (
        <tr key={tableItem.id}>
          {columns.map((column: Column<T>) => column.getContent(tableItem))}
        </tr>
      ))}
    </table>
  );
}

为了使用它,你可以传递泛型参数:

const COLUMNS: Column<Person>[] = [
  {
    title: "Name",
    getContent: (tableItem: Person) => {
      return <span style={{ color: "red" }}>{tableItem.name}</span>;
    },
  },
  {
    title: "Age",
    getContent: (tableItem: Person) => {
      return <span>{tableItem.age}</span>;
    },
  },
];

const App = () => {
  const data: Person[] = [
    {
      id: "1",
      name: "David",
      age: 20,
    },
    {
      id: "2",
      name: "Fiona",
      age: 24,
    },
  ];

  return <Table<Person> data={data} columns={COLUMNS} />;
};

然而,因为在上面的情况中明显可以从使用中推断出它,你可以省略它,让 TypeScript 推断它。

个人建议将 Person 定义为 interface Person extends TableItem { /* 更多属性 */ } 以使其更明确,但将其保留不变也应该可以工作。

英文:

You can move the generic "higher up" on the type definition:

export type TableItem = {
  id: string;
};

export type Column&lt;T extends TableItem&gt; = {
  title: string;
  getContent: (tableItem: T) =&gt; React.ReactNode;
};

type TableProps&lt;T extends TableItem&gt; = {
  data: T[];
  columns: Column&lt;T&gt;[];
};

Unfortunately this means you can no longer use FC because the generic definition is going to be on the lambda:

const Table = &lt;T extends TableItem&gt;({ data, columns }: TableProps&lt;T&gt;): ReactNode|null =&gt; {
  return (
    &lt;table&gt;
      &lt;th&gt;{columns.map((column: Column&lt;T&gt;) =&gt; column.title)}&lt;/th&gt;

      {data.map((tableItem: T) =&gt; (
        &lt;tr key={tableItem.id}&gt;
          {columns.map((column: Column&lt;T&gt;) =&gt; column.getContent(tableItem))}
        &lt;/tr&gt;
      ))}
    &lt;/table&gt;
  );

In order to use it you can pass the generic parameter:

const COLUMNS: Column&lt;Person&gt;[] = [
  {
    title: &quot;Name&quot;,

    getContent: (tableItem: Person) =&gt; {
      return &lt;span style={{ color: &quot;red&quot; }}&gt;{tableItem.name}&lt;/span&gt;;
    },
  },
  {
    title: &quot;Age&quot;,
    getContent: (tableItem: Person) =&gt; {
      return &lt;span&gt;{tableItem.age}&lt;/span&gt;;
    },
  },
];

const App = () =&gt; {
  const data: Person[] = [
    {
      id: &quot;1&quot;,
      name: &quot;David&quot;,
      age: 20,
    },
    {
      id: &quot;2&quot;,
      name: &quot;Fiona&quot;,
      age: 24,
    },
  ];

  return &lt;Table&lt;Person&gt; data={data} columns={COLUMNS} /&gt;;
};

however because it's rather obvious from use what that should be in the above case you can omit it and let TypeScript infer it.

Personally I'd define Person as interface Person extends TableItem { /* more props */ } to make it more explicit but I leaving it as is should also work

huangapple
  • 本文由 发表于 2023年2月14日 18:56:37
  • 转载请务必保留本文链接:https://go.coder-hub.com/75446838.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定