英文:
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 "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.
答案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<T extends TableItem> = {
title: string;
getContent: (tableItem: T) => React.ReactNode;
};
type TableProps<T extends TableItem> = {
data: T[];
columns: Column<T>[];
};
Unfortunately this means you can no longer use FC
because the generic definition is going to be on the 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>
);
In order to use it you can pass the generic parameter:
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} />;
};
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
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论