庞大的类型定义
在一次代码审查中,我发现了一个数据展示组件 DataDisplay 的data
属性类型定义异常复杂,它使用了联合类型:
tsx
import React from 'react';
import type { FC } from 'react';
type Data = string | number | boolean | string[] | number[] | { [key: string]: any };
interface DataDisplayProps {
data: Data;
renderData?: (data: Data) => React.ReactNode;
}
const DataDisplay: FC<DataDisplayProps> = ({ data, renderItem }) => {
if (typeof data === 'boolean') {
return <div>{data ? '是' : '否'}</div>;
}
// 其他数据格式的渲染逻辑
if (renderData) {
return <div>{renderData(data)}</div>;
}
return <div>渲染异常,请自定义渲染</div>;
};
export default DataDisplay;
出于好奇,我询问团队成员为什么要这样定义类型。他解释说:"考虑到数据类型的多样性,我认为这样做是必要的。"
我提出了担忧:"如果未来需要支持新的数据类型,你将不得不断扩展这个联合类型,这会使得类型定义变得越来越复杂。你有没有考虑过这一点?"
他有些无奈地回应:"那还能怎么办?你又不让我用any
来定义!"
这时,我提出了另一种方案:"其实,我们可以考虑使用泛型来定义。"
他显得有些惊讶:"泛型?我以为那只适用于函数参数。它也能用在组件属性上吗?"
我回答道:"当然可以,React函数组件本质上也是一个函数啊。"
泛型组件
将 DataDisplay 组件改造成一个泛型组件。代码如下所示:
tsx
import React from 'react';
interface DataDisplayProps<T> {
data: T;
renderData?: (data: T) => React.ReactNode;
}
const DataDisplay = <T,>({ data, renderItem }: DataDisplayProps<T>) => {
if (typeof data === 'boolean') {
return <div>{data ? '是' : '否'}</div>;
}
// 其他数据格式的渲染逻辑
if (renderData) {
return <div>{renderData(data)}</div>;
}
return <div>渲染异常,请自定义渲染</div>;
};
export default DataDisplay;
注意几个关键的改造:
- 移除了
FC
类型的使用 :泛型组件不能通过FC
直接声明。因为FC
类型不支持泛型参数,而直接定义的组件函数允许我们使用泛型。 - 使用
<T,>
避免语法混淆 :在 TypeScript 中,使用泛型时,<T>
语法可能与 JSX 的元素标签混淆。通过在泛型尖括号内加一个逗号,即<T,>
,可以清晰区分泛型和 JSX。
为什么要使用泛型组件
DataDisplay 组件被设计为能够处理多种数据类型,并且在遇到无法通过内置逻辑渲染的数据时,允许用户通过 renderData
属性传入一个自定义渲染函数。这种设计意味着 data
属性必须能够接受多样化的数据类型,从简单的基本类型到复杂的对象或数组,这使得使用联合类型来定义data
属性变得极其复杂且难以管理。
泛型组件提供了一种优雅的解决方案。通过引入泛型,我们可以让 DataDisplay 组件接受一个类型参数T
,这样组件就可以根据传入的具体类型来动态定义 data
属性的类型。
ts
interface DataDisplayProps<T> {
data: T;
renderData?: (data: T) => React.ReactNode;
}
特别地,这样做可以让 renderData
属性接收的函数的参数拥有一个明确的类型,并与 data
属性的类型完全一致。这样一来,在自定义渲染函数中处理参数(数据)时,我们能够更加清晰地了解数据结构,减少错误的发生。
举个例子来说明,假设需要渲染一个包含 {name: '小明', age: 15}
的数据。我们可以这么使用DataDisplay 组件:
tsx
import React from 'react';
import DataDisplay from '@/components/DataDisplay';
const App: React.FC = () => {
return (
<DataDisplay<{ name: string; age: number }>
data={{ name: '小明', age: 15 }}
renderData={data => <div>我叫{data.name},今年{data.age}岁</div>}
/>
);
};
export default App;
在这个示例中,通过 <DataDisplay<{ name: string; age: number }>
的语法允许我们将{ name: string; age: number }
这个具体的类型指定为 DataDisplay 组件的泛型参数T
,这样明确了 renderData
属性接收的自定义渲染函数的参数类型为 { name: string; age: number }
,保证了在自定义渲染函数中可以安全地操作这些参数(数据),假如在其中使用{name: '小明', age: 15}
数据中不存在的值,会直接提示错误,如下图所示:
题外话:泛型不仅限于T
虽然 T
是泛型参数的传统表示(代表"Type"),但我们可以使用任何有效的标识符命名泛型参数。例如,K
和 V
常用于表示键值对的键和值的类型,E
用于表示元素的类型等。
lua
function identity<T>(arg: T): T {
return arg;
}
function getKeyValue<K, V>(key: K, value: V): [K, V] {
return [key, value];
}
通过这种方式,泛型的使用不仅限于函数参数,也同样适用于组件属性,提供了更广泛的应用可能性和灵活性。