在上一篇文章中,我们探讨了React组件道具可以用TypeScript进行强类型化的不同方式。在这篇文章中,我们将介绍一种更高级的方法,即为道具使用一个通用类型。
使用案例
当你想创建一个可重复使用的数据驱动的组件时,通用道具很有用,因为数据的形状是不同的。可重用的表单、列表和表格组件就是通用道具很方便的例子。在这篇文章中,我们将创建一个简单的可重复使用的表格组件。
语法
通用道具类型的语法如下:
type Props<T> = { ... };
T 是消费者可以传入的数据类型的一个参数。当我们实现我们的例子时,我们将使用一个更有意义的名字而不是T。
如果我们想用一个接口来表示该类型,那么语法将如下:
interface Props<T> { ... }
更棘手的是将这个类型应用于React组件:
const ReusableComponent = <T extends object>(props: Props<T>) => { ... }
上面的语法是针对一个箭头函数组件的。下面是普通函数组件声明的语法:
function ReusableComponent<T extends object>(props: Props<T>) { ... }
我们在函数参数的括号前定义消费者可以传入组件的类型。注意,我们使用了T extends object ,而不仅仅是T ,否则会出现解析错误(我们也可以使用<T,> 来避免解析错误)。
如果我们想让数据有一个特定的属性,那么我们可以指定这个属性来代替object 。下面的例子要求数据有一个name 属性:
<T extends {name: string}>
现在我们开始了解语法了,让我们来看看一个用例。
通用的表格组件props
我们将实现一个通用的表格组件。这里是我们的通用道具类型:
type Props<DataItem> = {
data: DataItem[];
onRowClick: (item: DataItem) => void;
};
我们选择将类型参数命名为DataItem ,这是数据中每个项目的类型。道具包含一个data 道具,它是一个数据项的数组。道具还包含一个事件道具,onRowClick 。当表格中的某一行被点击时,该事件将被调用,并将该行的数据项传递给它。
下面是表格组件实现的开始:
const Table = <T extends object>({ data, onRowClick }: Props<T>) => {}
我们对data 和onRowClick 道具进行了重构,这样我们就可以在实现中直接引用它们。
我们的表组件可以被消费,如下所示:
<Table
data={[
{ name: "Bill", score: 15 },
{ name: "Jane", score: 18 },
{ name: "Fred", score: 10 }
]}
onRowClick={row => console.log(row)}
/>
请注意,我们不需要向组件传递数据类型,因为TypeScript会巧妙地推断它。
渲染表格的列标题
我们将渲染表格中的列标题。在这之前,我们将检查是否有任何数据,如果没有,我们将不渲染:
const Table = <DataItem extends object>({ data, onRowClick }: Props<DataItem>) => {
if (data.length === 0) { return null; }};
现在我们来渲染列标题:
const Table = <DataItem extends object>({ data, onRowClick }: Props<DataItem>) => {
if (data.length === 0) {
return null;
}
return ( <table> <thead> <tr> {Object.keys(data[0]).map(key => ( <th key={key}>{key}</th> ))} </tr> </thead> </table> );};
我们使用Object.keys 来获取数据中的所有属性,然后用map 方法来迭代它们,输出th 元素。
渲染表格数据
让我们继续渲染表中的数据:
const Table = <T extends object>({ data, onRowClick }: Props<T>) => {
...
return (
<table>
<thead>
...
</thead>
<tbody> {data.map((item: DataItem, index: number) => ( <tr key={index}> {(Object.keys(data[0]) as Array<keyof DataItem>).map(key => ( <td key={key.toString()}>{item[key]}</td> ))} </tr> ))} </tbody> </table>
);
};
请注意,我们必须在Object.keys 的结果上使用一个类型断言,因为默认情况下,它将返回string[] ,这意味着item[key] 会引发一个类型错误。
实现行点击
最后一步是实现行的点击:
<tbody>
{data.map((item: DataItem, index: number) => (
<tr key={index} onClick={() => onRowClick(item)}> {(Object.keys(data[0]) as Array<keyof DataItem>).map(key => (
<td key={key.toString()}>{item[key]}</td>
))}
</tr>
))}
</tbody>
让我们检查一下onRowClick 参数的类型在我们的消费者中被推断为什么。

很好!
这显示了通用道具的威力,因为即使传入的数据类型可能不同,道具对消费者来说仍然是强类型的。
这里有一个CodeSandbox中这个例子的链接。
总结
通用道具允许我们实现通用组件,在不影响类型安全的情况下处理不同形状的数据。这个语法在一开始可能有点奇怪,但过了一段时间后就很有意义了,而且其结果是非常值得的。