不是联合类型用不起,而是泛型性价比更高

庞大的类型定义

在一次代码审查中,我发现了一个数据展示组件 DataDisplaydata属性类型定义异常复杂,它使用了联合类型:

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"),但我们可以使用任何有效的标识符命名泛型参数。例如,KV 常用于表示键值对的键和值的类型,E 用于表示元素的类型等。

lua 复制代码
function identity<T>(arg: T): T {
    return arg;
}

function getKeyValue<K, V>(key: K, value: V): [K, V] {
    return [key, value];
}

通过这种方式,泛型的使用不仅限于函数参数,也同样适用于组件属性,提供了更广泛的应用可能性和灵活性。

相关推荐
come1123415 分钟前
Vue 响应式数据传递:ref、reactive 与 Provide/Inject 完全指南
前端·javascript·vue.js
前端风云志36 分钟前
TypeScript结构化类型初探
javascript
musk12121 小时前
electron 打包太大 试试 tauri , tauri 安装打包demo
前端·electron·tauri
翻滚吧键盘1 小时前
js代码09
开发语言·javascript·ecmascript
万少2 小时前
第五款 HarmonyOS 上架作品 奇趣故事匣 来了
前端·harmonyos·客户端
OpenGL2 小时前
Android targetSdkVersion升级至35(Android15)相关问题
前端
rzl022 小时前
java web5(黑马)
java·开发语言·前端
Amy.Wang2 小时前
前端如何实现电子签名
前端·javascript·html5
海天胜景2 小时前
vue3 el-table 行筛选 设置为单选
javascript·vue.js·elementui
今天又在摸鱼2 小时前
Vue3-组件化-Vue核心思想之一
前端·javascript·vue.js