likes
comments
collection
share

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

作者站长头像
站长
· 阅读数 2

庞大的类型定义

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

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 组件改造成一个泛型组件。代码如下所示:

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 属性的类型。

interface DataDisplayProps<T> {
  data: T;
  renderData?: (data: T) => React.ReactNode;
}

特别地,这样做可以让 renderData 属性接收的函数的参数拥有一个明确的类型,并与 data 属性的类型完全一致。这样一来,在自定义渲染函数中处理参数(数据)时,我们能够更加清晰地了解数据结构,减少错误的发生。

举个例子来说明,假设需要渲染一个包含 {name: '小明', age: 15} 的数据。我们可以这么使用DataDisplay 组件:

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 用于表示元素的类型等。

function identity<T>(arg: T): T {
    return arg;
}

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

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